@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
package/dist/model.js ADDED
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export const relativePath = z.string().min(1).transform(p => p.replace(/\/+$/, '')).pipe(z.string().min(1).refine(p => !p.startsWith('/') && !p.includes('\\') && !p.split('/').some(s => s === '..' || s === '.' || s === '') && !/^[A-Za-z]:/.test(p), 'Use a repository-relative path without traversal')).describe('Repository-relative path without traversal or symlinks; trailing directory slashes normalize away.');
3
+ const id = z.string().regex(/^[a-z0-9][a-z0-9-]*$/);
4
+ export const chapterKey = z.string().regex(/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/);
5
+ export const factKey = z.string().regex(/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/);
6
+ export const Evidence = z.object({ path: relativePath.describe('Supporting source path; may be outside the owning chapter. Evidence paths are automatically tracked for freshness without adding them to sourceScope.'), quote: z.string().trim().min(1).max(1200) }).strict();
7
+ const LegacyFact = z.object({ id, statement: z.string().trim().min(8).max(2000), evidence: z.array(Evidence).min(1).max(8) }).strict();
8
+ export const Fact = LegacyFact.extend({ sourceScope: z.array(relativePath).min(1).describe('Owned source coverage, restricted to this chapter paths. For a chapter owning src: sourceScope [src], evidence [{path: shared/config.ts, quote: mode = 1}]. Cross-chapter citations belong in evidence, not sourceScope.'), dependsOn: z.array(factKey).default([]) }).strict();
9
+ const Boundary = z.object({ id, title: z.string().trim().min(3).max(100), scope: z.string().trim().min(12).max(800), excludes: z.string().trim().min(3).max(800) }).strict();
10
+ export const ChapterDefinition = Boundary.extend({ paths: z.array(relativePath).min(1).max(30) }).strict();
11
+ export const Chapter = ChapterDefinition.extend({ revision: z.number().int().positive(), facts: z.array(Fact), sources: z.record(z.string()), dependencyFingerprints: z.record(z.string()).default({}) }).strict();
12
+ export const PillarDefinition = Boundary.extend({ chapters: z.array(ChapterDefinition).min(1) }).strict();
13
+ export const Pillar = Boundary.extend({ chapters: z.array(Chapter).min(1) }).strict();
14
+ export const Registry = z.object({ schemaVersion: z.literal(2), pillars: z.array(Pillar) }).strict();
15
+ export const Maintenance = z.object({ factId: id, action: z.enum(['correct', 'merge', 'remove', 'tighten']), reason: z.string().trim().min(12).max(1000), replacement: factKey.optional() }).strict();
16
+ export const Review = z.object({ chapterId: chapterKey, expectedRevision: z.number().int().positive(), invalidatedFactIds: z.array(z.string()), reviewedFactIds: z.array(z.string()), facts: z.array(Fact), reason: z.string().trim().min(12).max(1000), maintenance: z.array(Maintenance).optional() }).strict();
17
+ export const Verification = z.object({ sourceFiles: z.array(relativePath), documentFiles: z.array(relativePath) }).strict();
18
+ export const Update = z.object({ chapterId: chapterKey, factIds: z.array(id).min(1).optional(), touchedPaths: z.array(relativePath), reviews: z.array(Review).min(1), verification: Verification, tidyId: z.string().uuid().optional() }).strict();
19
+ export const LegacyRegistry = z.object({ schemaVersion: z.literal(1), pillars: z.array(Boundary.extend({ paths: z.array(relativePath).min(1).max(30), revision: z.number().int().positive(), facts: z.array(LegacyFact), sources: z.record(z.string()) }).strict()) }).strict();
20
+ export function unique(values, label) { if (new Set(values).size !== values.length)
21
+ throw new Error(`Duplicate ${label}`); }
@@ -0,0 +1,164 @@
1
+ import { type Store } from './store.js';
2
+ import { type RegistryRecord } from './model.js';
3
+ export interface RouteOptions {
4
+ path?: string;
5
+ signal?: string;
6
+ cursor?: string;
7
+ limit?: number;
8
+ }
9
+ export declare function ownershipMap(store: Store, options?: RouteOptions, suppliedRegistry?: RegistryRecord, cache?: Map<string, Promise<string>>): Promise<{
10
+ items: {
11
+ freshness: {
12
+ chapterId: string;
13
+ revision: number;
14
+ status: string;
15
+ locallyReviewed: boolean;
16
+ changedPaths: string[];
17
+ dependencyDrift: boolean;
18
+ error?: undefined;
19
+ } | {
20
+ chapterId: string;
21
+ revision: number;
22
+ status: string;
23
+ error: any;
24
+ locallyReviewed?: undefined;
25
+ changedPaths?: undefined;
26
+ dependencyDrift?: undefined;
27
+ };
28
+ pillarId: string;
29
+ chapterId: string;
30
+ title: string;
31
+ scope: string;
32
+ paths: string[];
33
+ match: string;
34
+ score: number;
35
+ matchedTerms: string[];
36
+ matchingFactCount: number;
37
+ factHints: string[];
38
+ revision: number;
39
+ }[];
40
+ total: number;
41
+ nextCursor: string | null;
42
+ basis: string;
43
+ ambiguous: boolean;
44
+ }>;
45
+ export declare function pillarGraph(store: Store, pillarId?: string, cursor?: string, limit?: number): Promise<{
46
+ items: {
47
+ freshness: string;
48
+ from: string;
49
+ to: string;
50
+ factLinkCount: number;
51
+ examples: {
52
+ from: string;
53
+ to: string;
54
+ }[];
55
+ }[];
56
+ total: number;
57
+ nextCursor: string | null;
58
+ direction: string;
59
+ basis: string;
60
+ }>;
61
+ export declare function startHere(store: Store, options?: RouteOptions): Promise<{
62
+ guide: string;
63
+ state: string;
64
+ principle: string;
65
+ next: string;
66
+ policy?: undefined;
67
+ routes?: undefined;
68
+ } | {
69
+ guide: string;
70
+ principle: string;
71
+ policy: string;
72
+ routes: {
73
+ items: {
74
+ freshness: {
75
+ chapterId: string;
76
+ revision: number;
77
+ status: string;
78
+ locallyReviewed: boolean;
79
+ changedPaths: string[];
80
+ dependencyDrift: boolean;
81
+ error?: undefined;
82
+ } | {
83
+ chapterId: string;
84
+ revision: number;
85
+ status: string;
86
+ error: any;
87
+ locallyReviewed?: undefined;
88
+ changedPaths?: undefined;
89
+ dependencyDrift?: undefined;
90
+ };
91
+ pillarId: string;
92
+ chapterId: string;
93
+ title: string;
94
+ scope: string;
95
+ paths: string[];
96
+ match: string;
97
+ score: number;
98
+ matchedTerms: string[];
99
+ matchingFactCount: number;
100
+ factHints: string[];
101
+ revision: number;
102
+ }[];
103
+ total: number;
104
+ nextCursor: string | null;
105
+ basis: string;
106
+ ambiguous: boolean;
107
+ };
108
+ next: string;
109
+ state?: undefined;
110
+ }>;
111
+ export declare function tidyPlan(store: Store, target: string, cursor?: string, limit?: number): Promise<{
112
+ target: string;
113
+ writesKnowledge: boolean;
114
+ agentActionRequired: boolean;
115
+ policy: string;
116
+ next: string;
117
+ requiredChapterCount: number;
118
+ chapters: {
119
+ items: {
120
+ chapterId: string;
121
+ revision: number;
122
+ factCount: number;
123
+ duplicateGroupCount: number;
124
+ duplicateHints: string[][];
125
+ }[];
126
+ total: number;
127
+ nextCursor: string | null;
128
+ };
129
+ }>;
130
+ /** Read-only mechanical validation. Evidence presence never proves semantic truth. */
131
+ export declare function validateKnowledge(store: Store, target?: string, cursor?: string, limit?: number, allResults?: boolean): Promise<{
132
+ items: {
133
+ dependencyIssues: {
134
+ recordChanged: boolean;
135
+ changedPaths: string[];
136
+ errors: string[];
137
+ factId: string;
138
+ }[];
139
+ locallyReviewed: boolean;
140
+ changedPaths: string[];
141
+ errors: string[];
142
+ factId: string;
143
+ chapterId: string;
144
+ status: string;
145
+ }[];
146
+ total: number;
147
+ nextCursor: string | null;
148
+ target: string;
149
+ writesKnowledge: boolean;
150
+ valid: boolean;
151
+ basis: string;
152
+ affected: {
153
+ pillars: string[];
154
+ chapters: string[];
155
+ facts: string[];
156
+ };
157
+ cleanupPrompt: string | null;
158
+ summary: {
159
+ selectedFacts: number;
160
+ checkedFacts: number;
161
+ needsReview: number;
162
+ unpopulatedChapters: string[];
163
+ };
164
+ }>;
@@ -0,0 +1,164 @@
1
+ import { same } from './store.js';
2
+ import { relativePath } from './model.js';
3
+ import { page } from './paging.js';
4
+ const words = (text) => [...new Set(text.toLocaleLowerCase().split(/[^\p{L}\p{N}_]+/u).filter(Boolean))];
5
+ const contains = (scope, file) => scope === file || file.startsWith(`${scope}/`);
6
+ export async function ownershipMap(store, options = {}, suppliedRegistry, cache) {
7
+ if (options.path)
8
+ options = { ...options, path: relativePath.parse(options.path) };
9
+ const registry = suppliedRegistry ?? await store.read();
10
+ const terms = words(options.signal ?? '').filter(t => !['a', 'an', 'the', 'is', 'my', 'this', 'what', 'do', 'failed', 'failing'].includes(t));
11
+ const entries = store.chapters(registry).map(({ key, pillar, chapter }) => {
12
+ const paths = options.path ? chapter.paths.filter(p => contains(p, options.path)) : chapter.paths;
13
+ const boundary = words(`${pillar.title} ${pillar.scope} ${chapter.title} ${chapter.scope} ${chapter.paths.join(' ')}`);
14
+ const matchingFacts = chapter.facts.map(f => {
15
+ const tokens = words(`${f.statement} ${f.evidence.map(e => `${e.path} ${e.quote}`).join(' ')}`);
16
+ return { factId: f.id, terms: terms.filter(t => tokens.includes(t)) };
17
+ }).filter(f => f.terms.length);
18
+ const boundaryTerms = terms.filter(t => boundary.includes(t));
19
+ const matchedTerms = [...new Set([...boundaryTerms, ...matchingFacts.flatMap(f => f.terms)])];
20
+ return { pillarId: pillar.id, chapterId: key, title: chapter.title, scope: chapter.scope,
21
+ paths, match: paths.length && options.path ? 'registered-path' : terms.length ? 'keyword-hint' : 'index',
22
+ score: (options.path && paths.length ? 1000 : 0) + matchedTerms.length,
23
+ matchedTerms, matchingFactCount: matchingFacts.length,
24
+ factHints: matchingFacts.slice(0, 3).map(f => `${key}/${f.factId}`), revision: chapter.revision };
25
+ }).filter(entry => options.path ? entry.paths.length > 0 : options.signal ? entry.matchedTerms.length > 0 : true)
26
+ .sort((a, b) => b.score - a.score || a.chapterId.localeCompare(b.chapterId));
27
+ const result = page(entries, options.cursor, options.limit, JSON.stringify(options.path ?? '') + (options.signal ?? ''));
28
+ return { basis: 'Registered chapter paths define ownership. Signal matches are keyword hints from boundaries, facts, and evidence; verify source before diagnosing.',
29
+ ambiguous: entries.length > 1, ...result,
30
+ items: await Promise.all(result.items.map(async (item) => ({ ...item, freshness: await store.status(item.chapterId, registry, cache) }))) };
31
+ }
32
+ export async function pillarGraph(store, pillarId, cursor, limit) {
33
+ const registry = await store.read();
34
+ if (pillarId && !registry.pillars.some(p => p.id === pillarId))
35
+ throw new Error('Unknown pillar');
36
+ const edges = new Map();
37
+ for (const entry of store.facts(registry))
38
+ for (const dependency of entry.fact.dependsOn) {
39
+ const from = entry.key.split('/')[0], to = dependency.split('/')[0];
40
+ if (from === to || (pillarId && from !== pillarId && to !== pillarId))
41
+ continue;
42
+ const key = `${from}/${to}`;
43
+ const edge = edges.get(key) ?? { from, to, factLinkCount: 0, examples: [], chapters: new Set() };
44
+ edge.factLinkCount++;
45
+ if (edge.examples.length < 3)
46
+ edge.examples.push({ from: entry.key, to: dependency });
47
+ edge.chapters.add(entry.chapterId);
48
+ edge.chapters.add(dependency.slice(0, dependency.lastIndexOf('/')));
49
+ edges.set(key, edge);
50
+ }
51
+ const selected = page([...edges.values()].sort((a, b) => `${a.from}/${a.to}`.localeCompare(`${b.from}/${b.to}`))
52
+ .map(({ chapters, ...edge }) => ({ ...edge, chapterIds: [...chapters].sort() })), cursor, limit);
53
+ const states = new Map();
54
+ for (const edge of selected.items)
55
+ for (const key of edge.chapterIds)
56
+ if (!states.has(key))
57
+ states.set(key, await store.status(key, registry));
58
+ return { direction: 'dependent pillar -> dependency pillar',
59
+ basis: 'Derived from recorded cross-pillar fact dependencies. Missing edges mean unrecorded relationships, not proven independence. Cycles are allowed.',
60
+ ...selected, items: selected.items.map(({ chapterIds, ...edge }) => ({ ...edge,
61
+ freshness: chapterIds.some(k => states.get(k).status !== 'evidence-unchanged') ? 'needs-review' : 'evidence-unchanged' })) };
62
+ }
63
+ export async function startHere(store, options = {}) {
64
+ let routes;
65
+ try {
66
+ routes = await ownershipMap(store, options);
67
+ }
68
+ catch (error) {
69
+ if (error.code !== 'ENOENT')
70
+ throw error;
71
+ return { guide: '.common-ground/START_HERE.md', state: 'awaiting-bootstrap',
72
+ principle: 'Code is the source of truth. Common Ground is a cache that supplements source reading.',
73
+ next: 'Run cground init if needed, read directory READMEs and source, then refine the local bootstrap proposal with the developer before approving boundaries and seeding verified facts.' };
74
+ }
75
+ return { guide: '.common-ground/START_HERE.md',
76
+ principle: 'Code is the source of truth. Common Ground is a cache that supplements source reading.',
77
+ policy: '.common-ground/POLICY.md', routes,
78
+ next: routes.total === 0 ? 'No recorded owner matched. Inspect directory READMEs and code; ask the developer about ownership. Do not invent a pillar or diagnosis.'
79
+ : routes.ambiguous ? 'Multiple candidates matched. Narrow with a repository-relative path and read their scopes before choosing.'
80
+ : 'Call list_chapters for the matched pillar, then read_chapter and read_fact; verify their source.' };
81
+ }
82
+ export async function tidyPlan(store, target, cursor, limit) {
83
+ const registry = await store.read();
84
+ const keys = store.resolveTarget(registry, target);
85
+ const required = [...new Set(keys.flatMap(key => store.related(registry, key)))].sort();
86
+ const chapters = page(required.map(key => {
87
+ const c = store.chapter(registry, key);
88
+ const groups = new Map();
89
+ for (const f of c.facts) {
90
+ const normalized = words(f.statement).sort().join(' ');
91
+ groups.set(normalized, [...(groups.get(normalized) ?? []), f.id]);
92
+ }
93
+ const duplicates = [...groups.values()].filter(ids => ids.length > 1);
94
+ return { chapterId: key, revision: c.revision, factCount: c.facts.length,
95
+ duplicateGroupCount: duplicates.length, duplicateHints: duplicates.slice(0, 3).map(ids => ids.slice(0, 5)) };
96
+ }), cursor, limit, JSON.stringify(registry));
97
+ return { target, writesKnowledge: false, agentActionRequired: true,
98
+ policy: 'Skim every fact page in each required chapter. Read source now, merge verified near duplicates, remove superseded content, tighten narrative, correct in place, and repair references atomically. Similar wording is only a hint. If verification is ambiguous, ask; do not guess.',
99
+ next: 'Use review_checklist for these chapter IDs and touched paths. A developer-requested cground tidy TARGET or MCP cground operation tidy issues a local tidyId for prepare_update or prepare_patch; tidy_plan itself only previews scope.',
100
+ requiredChapterCount: required.length, chapters };
101
+ }
102
+ /** Read-only mechanical validation. Evidence presence never proves semantic truth. */
103
+ export async function validateKnowledge(store, target = 'all', cursor, limit, allResults = true) {
104
+ const registry = await store.read(); // Schema, ownership and dependency references are checked globally.
105
+ const chapters = store.resolveTarget(registry, target);
106
+ const allFacts = store.facts(registry), index = new Map(allFacts.map(f => [f.key, f]));
107
+ const factTarget = target === 'all' ? undefined : allFacts.find(f => f.key === target || f.fact.id === target);
108
+ const selected = factTarget ? [factTarget] : allFacts.filter(f => chapters.includes(f.chapterId));
109
+ const required = [...new Set(selected.flatMap(f => [f.key, ...store.upstreamFacts(registry, f.key, index)]))];
110
+ const checked = new Map();
111
+ const states = new Map();
112
+ const fingerprints = new Map();
113
+ for (const key of required) {
114
+ const { chapterId, chapter, fact } = index.get(key);
115
+ const errors = [];
116
+ try {
117
+ await store.validateFacts({ paths: chapter.paths, facts: [fact] });
118
+ }
119
+ catch (e) {
120
+ errors.push(e.message);
121
+ }
122
+ let changedPaths = [];
123
+ try {
124
+ const current = await store.snapshot({ paths: chapter.paths, facts: [fact] });
125
+ const baseline = Object.fromEntries(Object.entries(chapter.sources).filter(([file]) => fact.sourceScope.some(scope => contains(scope, file)) || fact.evidence.some(e => e.path === file)));
126
+ changedPaths = [...new Set([...Object.keys(current), ...Object.keys(baseline)])].filter(file => current[file] !== baseline[file]).sort();
127
+ }
128
+ catch (e) {
129
+ if (!errors.includes(e.message))
130
+ errors.push(e.message);
131
+ }
132
+ checked.set(key, { changedPaths, errors });
133
+ if (!states.has(chapterId)) {
134
+ states.set(chapterId, await store.status(chapterId, registry));
135
+ fingerprints.set(chapterId, store.dependencyFingerprints(registry, chapterId));
136
+ }
137
+ }
138
+ const items = selected.map(({ key, chapterId, chapter }) => {
139
+ const own = checked.get(key);
140
+ const dependencies = store.upstreamFacts(registry, key, index);
141
+ const dependencyIssues = dependencies.filter(dep => {
142
+ const state = checked.get(dep);
143
+ return state.errors.length || state.changedPaths.length || chapter.dependencyFingerprints[dep] !== fingerprints.get(chapterId)[dep];
144
+ }).map(factId => ({ factId, ...checked.get(factId), recordChanged: chapter.dependencyFingerprints[factId] !== fingerprints.get(chapterId)[factId] }));
145
+ const locallyReviewed = states.get(chapterId).locallyReviewed === true;
146
+ const invalidEvidence = own.errors.length > 0 || dependencyIssues.some(d => d.errors.length > 0);
147
+ const drift = own.changedPaths.length > 0 || dependencyIssues.length > 0;
148
+ return { factId: key, chapterId, status: invalidEvidence || (drift && !locallyReviewed) ? 'needs-review' : 'evidence-unchanged',
149
+ ...own, dependencyIssues, locallyReviewed };
150
+ });
151
+ const unpopulatedChapters = chapters.filter(key => !store.chapter(registry, key).facts.length);
152
+ const stale = items.filter(item => item.status === 'needs-review');
153
+ const affected = { pillars: [...new Set(stale.map(item => item.chapterId.split('/')[0]))].sort(), chapters: [...new Set(stale.map(item => item.chapterId))].sort(), facts: stale.map(item => item.factId).sort() };
154
+ const needsReview = items.filter(item => item.status === 'needs-review').length;
155
+ // Do not return success for a registry that changed during a potentially long scan.
156
+ if (!same(registry, await store.read()))
157
+ throw new Error('Knowledge changed during validation; rerun validate.');
158
+ return { target, writesKnowledge: false, valid: items.length > 0 && needsReview === 0 && unpopulatedChapters.length === 0,
159
+ basis: 'Checks registry structure, exact evidence, source scopes and upstream dependencies. This does not prove the assertions are true.',
160
+ affected,
161
+ cleanupPrompt: needsReview ? "Start automatic cleanup?" : null,
162
+ summary: { selectedFacts: items.length, checkedFacts: required.length, needsReview, unpopulatedChapters },
163
+ ...page(allResults ? items : stale, cursor, limit, JSON.stringify({ target, registry, allResults }), 12000) };
164
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ import { Store } from './store.js';
3
+ type Operation = {
4
+ description: string;
5
+ schema: z.AnyZodObject;
6
+ run: (args: any) => Promise<unknown>;
7
+ };
8
+ export declare function operations(store: Store): Record<string, Operation>;
9
+ export declare function runOperation(store: Store, operation: string, args?: unknown): Promise<unknown>;
10
+ export {};
@@ -0,0 +1,130 @@
1
+ import { Lookup, Assess, lookup, assessChanges } from './access.js';
2
+ import { toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';
3
+ import { reviewKnowledge } from './review.js';
4
+ import { ReadKnowledge, readKnowledge, listPillars, listChapters, readChapter, readFact, search, reviewChecklist } from './retrieval.js';
5
+ import { z } from 'zod';
6
+ import { Fact, PillarDefinition, ChapterDefinition, Update, chapterKey, relativePath } from './model.js';
7
+ import { Workflow, Patch, Admission } from './workflow.js';
8
+ import { initialize, discover, guidanceStatus, refreshGuidance } from './init.js';
9
+ import { checkHook, hookNotifications, installHook } from './hooks.js';
10
+ import { startHere, ownershipMap, pillarGraph } from './navigation.js';
11
+ import { checkKnowledge, startTidy } from './maintenance.js';
12
+ import { refreshKnowledgeExport } from './export.js';
13
+ import { version } from './version.js';
14
+ import { SourceSearch, sourceSearch } from './source-search.js';
15
+ const paging = { cursor: z.string().optional(), limit: z.number().int().min(1).max(20).optional() };
16
+ const target = { target: z.string().min(1) };
17
+ const task = { taskId: z.string().uuid() };
18
+ const approval = { approved: z.literal(true).describe('Declaration of explicit developer approval of this exact operation and content; never infer approval from this flag.') };
19
+ const batch = { batches: z.array(z.object({ chapterId: chapterKey, facts: z.array(Fact).min(1) }).strict()).min(1), dryRun: z.boolean().default(false), approved: z.literal(true).describe('Declaration of developer direction covering the initial boundaries and facts, through content approval or explicit delegation to publish within scope. Not proof of human review.').optional(), preflight: z.string().optional() };
20
+ const routing = { path: relativePath.optional(), signal: z.string().optional(), ...paging };
21
+ export function operations(store) {
22
+ const flow = new Workflow(store);
23
+ const op = (description, shape, run) => ({ description, schema: z.object(shape).strict(), run });
24
+ const mutation = async (operation, args, write) => {
25
+ const result = await write();
26
+ if (args.verbose)
27
+ return result;
28
+ const chapterIds = operation === 'approve' ? args.pillars.flatMap((p) => p.chapters.map((c) => `${p.id}/${c.id}`)) : operation === 'approve-chapters' ? args.chapters.map((c) => `${args.pillarId}/${c.id}`) : [args.chapterId];
29
+ const factIds = (args.facts ?? []).map((f) => `${args.chapterId}/${f.id}`);
30
+ return { operation, approved: true, chapterCount: chapterIds.length, chapterIds, factCount: factIds.length, factIds, validation: 'structure and exact evidence; not semantic verification', outputPath: '.common-ground/knowledge.json', next: operation.startsWith('approve') ? 'Boundaries approved; facts still require developer review and approval.' : 'Run validate, then use lookup as needed. Task contexts are optional.' };
31
+ };
32
+ const verbose = { verbose: z.boolean().default(false) };
33
+ const check = op('Check all or selected knowledge for structure, exact evidence and freshness. Stale results list affected IDs and offer cleanup. cleanup:true requires developer-requested cleanup; it creates a scoped tidyId for the calling agent to verify and submit corrections.', { target: z.string().default('all'), cleanup: z.boolean().default(false), allResults: z.boolean().default(false), ...paging }, a => checkKnowledge(store, a.target, a.cleanup, a.cursor, a.limit, undefined, a.allResults));
34
+ return {
35
+ lookup: op('Find compact facts, explicit coverage and navigation without scanning source. verify:true checks fact freshness; verbose:true adds matching diagnostics. Weak matches suggest a separate source-search operation.', Lookup.shape, a => lookup(store, a)),
36
+ 'source-search': op('Search explicitly selected files/directories for live source evidence, separate from stored facts. Bounded read-only search; no knowledge or task writes.', SourceSearch.shape, a => sourceSearch(store, a)),
37
+ assess: op('Read-only assessment of actual task-touched paths. No task needed. Compare candidate source changes; review:true supplies complete chapters and verification paths before corrections.', Assess.shape, a => assessChanges(store, a)),
38
+ schema: op('Inspect an operation input schema and CLI payload guidance.', { operation: z.string(), both: z.boolean().default(false) }, async (a) => {
39
+ const entry = operations(store)[a.operation];
40
+ if (!entry)
41
+ throw new Error('Unknown operation.');
42
+ const payload = ['seed', 'admit', 'propose-facts'].includes(a.operation) ? entry.schema.shape.facts
43
+ : a.operation === 'approve' ? entry.schema.pick({ pillars: true })
44
+ : a.operation === 'approve-chapters' ? entry.schema.pick({ chapters: true })
45
+ : a.operation === 'accept-facts' ? entry.schema.shape.review
46
+ : ['bootstrap', 'seed-batch'].includes(a.operation) ? entry.schema.omit({ dryRun: true, approved: true, preflight: true }) : entry.schema;
47
+ return { operation: a.operation, description: entry.description, ...(a.both ? { inputSchema: toJsonSchemaCompat(entry.schema) } : {}), cliPayloadSchema: toJsonSchemaCompat(payload) };
48
+ }),
49
+ bootstrap: op('Draft an initial map and facts without writes. Publication requires developer content approval or explicit delegation to publish within scope, plus the matching preflight token.', { pillars: z.array(PillarDefinition).min(1), ...batch }, a => {
50
+ if (!a.dryRun && a.approved !== true)
51
+ throw new Error('Developer approval required for boundaries and facts.');
52
+ return store.bootstrap(a.pillars, a.batches, a.dryRun, a.preflight);
53
+ }),
54
+ 'seed-batch': op('Preflight and populate multiple approved empty chapters atomically.', batch, a => {
55
+ if (!a.dryRun && a.approved !== true)
56
+ throw new Error('Developer approval required for facts.');
57
+ return store.bootstrap(undefined, a.batches, a.dryRun, a.preflight);
58
+ }),
59
+ init: op('Initialize or refresh repository setup; report completed/failed steps and continue when advisory hook permissions are unavailable.', { skipHook: z.boolean().default(false) }, a => initialize(store, a)),
60
+ 'refresh-guidance': op('Refresh only managed guidance; preserve surrounding content and shared knowledge.', {}, () => refreshGuidance(store)),
61
+ scan: op('Discover a proposed responsibility map; no approval or shared knowledge write.', { exclude: z.array(relativePath).default([]) }, a => discover(store, a.exclude)),
62
+ approve: op('Approve the developer-reviewed responsibility map.', { pillars: z.array(PillarDefinition), standaloneReason: z.string().optional(), ...approval, ...verbose }, a => mutation('approve', a, () => store.approveDefinitions(a.pillars, a.standaloneReason))),
63
+ migrate: op('Migrate a legacy registry after developer approval.', approval, () => store.migrate()),
64
+ 'approve-chapters': op('Add developer-approved chapters.', { pillarId: z.string(), chapters: z.array(ChapterDefinition), ...approval, ...verbose }, a => mutation('approve-chapters', a, () => store.addChapters(a.pillarId, a.chapters))),
65
+ seed: op('Populate an approved empty chapter with developer-approved source-verified facts.', { chapterId: chapterKey, facts: z.array(Fact), ...approval, ...verbose }, a => mutation('seed', a, () => store.seed(a.chapterId, a.facts))),
66
+ admit: op('Admit developer-approved facts after reviewing the whole chapter and source.', { chapterId: chapterKey, facts: z.array(Fact), ...approval, ...verbose }, a => mutation('admit', a, () => store.admit(a.chapterId, a.facts))),
67
+ 'task-start': op('Optional task context for deferred additions or aggregated correction reports; lookup and assess need no task.', { paths: z.array(relativePath).optional(), signal: z.string().optional() }, a => flow.start(a.paths, a.signal)),
68
+ 'task-assess': op('Assess actual task-touched paths.', { ...task, paths: z.array(relativePath), refresh: z.boolean().optional(), ...paging }, a => flow.assess(a.taskId, a.paths, a.cursor, a.limit, a.refresh)),
69
+ 'task-finish': op('Finish task; present compact before/after corrections with reasons, sources and recommendations. Only pending additions need approval; never ask the developer to review JSON.', { ...task, ...paging }, a => flow.finish(a.taskId, a.cursor, a.limit)),
70
+ 'read-knowledge': op('Read bounded knowledge; discover read kinds in the read_knowledge tool or help.', ReadKnowledge.shape, a => readKnowledge(store, a)),
71
+ 'prepare-patch': op('Prepare verified existing-fact maintenance after whole chapter and source review. taskId is optional; omitted means no start/finish lifecycle.', Patch.shape, a => flow.prepare(a)),
72
+ 'propose-facts': op('Queue source-verified facts locally for later approval.', { ...task, chapterId: chapterKey, facts: z.array(Fact).min(1) }, a => flow.propose(a.taskId, a.chapterId, a.facts)),
73
+ 'drop-facts': op('Discard selected local proposal keys.', { ...task, factKeys: z.array(z.string()).min(1) }, a => flow.drop(a.taskId, a.factKeys)),
74
+ 'accept-facts': op('Admit the explicitly approved finished-task batch after complete linked-chapter and source review.', { ...task, review: Admission, ...approval }, a => flow.accept(a.taskId, a.review)),
75
+ start: op('Route from a source path or symptom.', routing, a => startHere(store, a)),
76
+ owners: op('Read ownership routing hints.', routing, a => ownershipMap(store, a)),
77
+ graph: op('Read the recorded dependency graph.', { pillarId: z.string().optional(), ...paging }, a => pillarGraph(store, a.pillarId, a.cursor, a.limit)),
78
+ review: op('Summarize meaningful pillar/chapter/fact changes against Git HEAD, omitting unchanged records and metadata. Present the delta, verified reason, source links and recommendation in chat; no JSON editing. This is a diff, not proof of truth or approval.', { target: z.string().default('all'), staged: z.boolean().default(false), evidence: z.boolean().default(false), ...paging }, a => reviewKnowledge(store, a.target, a.staged, a.evidence, a.cursor, a.limit)),
79
+ check,
80
+ validate: check,
81
+ tidy: op('Start developer-requested cleanup for all, pillar, chapter or fact; returns tidyId. Calling agent must verify source and submit corrections. No internal model runs.', { ...target, ...paging }, a => startTidy(store, a.target, a.cursor, a.limit)),
82
+ 'review-checklist': op('List source and documentation to read this session.', { chapterIds: z.array(chapterKey), touchedPaths: z.array(relativePath), ...paging }, a => reviewChecklist(store, a.chapterIds, a.touchedPaths, a.cursor, a.limit)),
83
+ list: op('List pillars.', paging, a => listPillars(store, a.cursor, a.limit)),
84
+ chapters: op('List chapters in a pillar.', { pillarId: z.string(), ...paging }, a => listChapters(store, a.pillarId, a.cursor, a.limit)),
85
+ read: op('Read every chapter page before editing; fact retrieves evidence.', { chapterId: chapterKey, ...paging }, a => readChapter(store, a.chapterId, a.cursor, a.limit)),
86
+ fact: op('Read a fact and evidence.', { chapterId: chapterKey, factId: z.string() }, a => readFact(store, a.chapterId, a.factId)),
87
+ search: op('Search fact summaries.', { query: z.string().min(1), chapterId: chapterKey.optional(), limit: paging.limit }, a => search(store, a.query, a.limit, a.chapterId)),
88
+ 'review-plan': op('Find required linked chapter reviews.', { chapterId: chapterKey, factIds: Update.shape.factIds }, a => store.reviewPlan(a.chapterId, a.factIds)),
89
+ prepare: op('Prepare a complete verified review transaction.', Update.shape, a => store.prepare(a)),
90
+ commit: op('Publish a prepared transaction after source and revision conflict checks.', { proposalId: z.string().uuid() }, a => store.commit(a.proposalId)),
91
+ 'hook-check': op('Run the advisory staged-index hook check without prompting or blocking a commit.', {}, async () => ({ message: await checkHook(store) })),
92
+ 'hook-install': op('Install or inspect integration of the default advisory pre-commit hook.', {}, () => installHook(store)),
93
+ 'hook-mute': op('Suppress this checkout\'s hook notifications at developer request.', {}, () => hookNotifications(store, false)),
94
+ 'hook-unmute': op('Restore this checkout\'s hook notifications at developer request.', {}, () => hookNotifications(store, true)),
95
+ export: op('Refresh the complete Git-ignored local Markdown reference.', {}, () => refreshKnowledgeExport(store)),
96
+ doctor: op('Check expected files and registry validity; not a complete host diagnostic.', {}, async () => {
97
+ const checks = { node: process.version, root: store.root };
98
+ let valid = true;
99
+ for (const file of ['AGENTS.md', '.github/copilot-instructions.md', '.vscode/mcp.json', '.common-ground/knowledge.json']) {
100
+ try {
101
+ await store.safe(file);
102
+ checks[file] = 'present';
103
+ }
104
+ catch {
105
+ checks[file] = 'missing';
106
+ valid = false;
107
+ }
108
+ }
109
+ try {
110
+ checks.pillars = (await store.read()).pillars.length;
111
+ }
112
+ catch (e) {
113
+ checks.registryError = e.message;
114
+ valid = false;
115
+ }
116
+ const guidance = await guidanceStatus(store);
117
+ if (guidance.status !== 'current')
118
+ valid = false;
119
+ return { valid, ...checks, guidance, next: valid ? null : checks.registryError ? 'Review the registry error and repair it from source or Git history.' : guidance.next ?? 'Run cground init to repair missing setup.' };
120
+ }),
121
+ version: op('Read framework version.', {}, async () => ({ version })),
122
+ };
123
+ }
124
+ export async function runOperation(store, operation, args = {}) {
125
+ const catalog = operations(store);
126
+ const entry = Object.hasOwn(catalog, operation) ? catalog[operation] : undefined;
127
+ if (!entry)
128
+ throw new Error(`Unknown operation: ${operation}. Call cground help.`);
129
+ return entry.run(entry.schema.parse(args));
130
+ }
@@ -0,0 +1,5 @@
1
+ export declare function page<T>(items: T[], cursor?: string, limit?: number, version?: string, maxChars?: number): {
2
+ items: T[];
3
+ total: number;
4
+ nextCursor: string | null;
5
+ };
package/dist/paging.js ADDED
@@ -0,0 +1,32 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function page(items, cursor, limit = 10, version = '', maxChars = Infinity) {
3
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20)
4
+ throw new Error('Page size must be between 1 and 20');
5
+ const fingerprint = createHash('sha256').update(JSON.stringify({ version, items })).digest('hex');
6
+ let offset = 0;
7
+ if (cursor) {
8
+ let parsed;
9
+ try {
10
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString());
11
+ }
12
+ catch {
13
+ throw new Error('Invalid cursor');
14
+ }
15
+ if (parsed.fingerprint !== fingerprint)
16
+ throw new Error('Content changed; restart pagination.');
17
+ offset = parsed.offset;
18
+ if (!Number.isInteger(offset) || offset < 0 || offset > items.length)
19
+ throw new Error('Invalid cursor');
20
+ }
21
+ let next = offset, size = 0;
22
+ while (next < items.length && next < offset + limit) {
23
+ const length = JSON.stringify(items[next]).length;
24
+ // Always return at least one complete record; never truncate evidence or dependencies.
25
+ if (next > offset && size + length > maxChars)
26
+ break;
27
+ size += length;
28
+ next++;
29
+ }
30
+ return { items: items.slice(offset, next), total: items.length,
31
+ nextCursor: next < items.length ? Buffer.from(JSON.stringify({ offset: next, fingerprint })).toString('base64url') : null };
32
+ }