@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/SETUP.md +219 -0
- package/dist/access.d.ts +172 -0
- package/dist/access.js +175 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +198 -0
- package/dist/commands.d.ts +189 -0
- package/dist/commands.js +202 -0
- package/dist/discovery.d.ts +73 -0
- package/dist/discovery.js +417 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +22 -0
- package/dist/export.d.ts +14 -0
- package/dist/export.js +86 -0
- package/dist/guidance.d.ts +11 -0
- package/dist/guidance.js +75 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +141 -0
- package/dist/init.d.ts +304 -0
- package/dist/init.js +150 -0
- package/dist/maintenance.d.ts +126 -0
- package/dist/maintenance.js +23 -0
- package/dist/matching.d.ts +17 -0
- package/dist/matching.js +32 -0
- package/dist/model.d.ts +974 -0
- package/dist/model.js +21 -0
- package/dist/navigation.d.ts +164 -0
- package/dist/navigation.js +164 -0
- package/dist/operations.d.ts +10 -0
- package/dist/operations.js +130 -0
- package/dist/paging.d.ts +5 -0
- package/dist/paging.js +32 -0
- package/dist/retrieval.d.ts +146 -0
- package/dist/retrieval.js +150 -0
- package/dist/review-files.d.ts +3 -0
- package/dist/review-files.js +106 -0
- package/dist/review.d.ts +86 -0
- package/dist/review.js +124 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +105 -0
- package/dist/source-search.d.ts +63 -0
- package/dist/source-search.js +245 -0
- package/dist/store.d.ts +452 -0
- package/dist/store.js +718 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/workflow.d.ts +450 -0
- package/dist/workflow.js +317 -0
- package/docs/architecture.md +65 -0
- package/docs/audit-0.4.0.md +42 -0
- package/docs/demo.md +42 -0
- package/docs/discovery.md +70 -0
- package/docs/knowledge-policy.md +51 -0
- package/docs/pillar-contract.md +98 -0
- package/docs/quiet-workflow.md +98 -0
- package/docs/releases.md +157 -0
- package/package.json +52 -0
- package/schemas/admission.schema.json +75 -0
- package/schemas/knowledge.schema.json +192 -0
- package/schemas/patch.schema.json +220 -0
- package/schemas/update.schema.json +218 -0
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { GroundError } from './errors.js';
|
|
2
|
+
import { changedFacts } from './access.js';
|
|
3
|
+
import { reviewDocuments } from './review-files.js';
|
|
4
|
+
import { factReview } from './review.js';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { bootstrapNext } from './guidance.js';
|
|
9
|
+
import { same } from './store.js';
|
|
10
|
+
import { Fact, Maintenance, Verification, Update, chapterKey, relativePath, unique } from './model.js';
|
|
11
|
+
import { page } from './paging.js';
|
|
12
|
+
import { ownershipMap } from './navigation.js';
|
|
13
|
+
const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
14
|
+
const within = (file, scope) => file === scope || file.startsWith(`${scope}/`);
|
|
15
|
+
const overlaps = (a, b) => within(a, b) || within(b, a);
|
|
16
|
+
const revisionReview = z.object({ chapterId: chapterKey, expectedRevision: z.number().int().positive(), reviewedAllFacts: z.literal(true) }).strict();
|
|
17
|
+
export const Patch = z.object({
|
|
18
|
+
taskId: z.string().uuid().optional(), chapterId: chapterKey, factIds: Update.shape.factIds,
|
|
19
|
+
touchedPaths: z.array(relativePath), tidyId: z.string().uuid().optional(), verification: Verification,
|
|
20
|
+
reviews: z.array(revisionReview.extend({
|
|
21
|
+
replacements: z.array(Fact).default([]), removeFactIds: z.array(z.string()).default([]),
|
|
22
|
+
reason: z.string().trim().min(12).max(1000), maintenance: z.array(Maintenance).optional(),
|
|
23
|
+
}).strict()).min(1),
|
|
24
|
+
}).strict();
|
|
25
|
+
export const Admission = z.object({ reviews: z.array(revisionReview).min(1), verification: Verification }).strict();
|
|
26
|
+
/** Ignored, task-scoped bookkeeping. No prompt history or model calls; only changed fact records and task metadata. */
|
|
27
|
+
export class Workflow {
|
|
28
|
+
store;
|
|
29
|
+
constructor(store) {
|
|
30
|
+
this.store = store;
|
|
31
|
+
}
|
|
32
|
+
name(id) { return `local/task-${z.string().uuid().parse(id)}.json`; }
|
|
33
|
+
async task(id) {
|
|
34
|
+
const name = this.name(id);
|
|
35
|
+
await this.store.safe(`.common-ground/${name}`);
|
|
36
|
+
return JSON.parse(await fs.readFile(this.store.file(name), 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
async save(id, task) { await this.store.atomic(this.name(id), task); }
|
|
39
|
+
async reuse(id, key, value, context, refresh = false) {
|
|
40
|
+
if (!id)
|
|
41
|
+
return value;
|
|
42
|
+
return this.store.lock(async () => {
|
|
43
|
+
const task = await this.task(id), reference = digest(key).slice(0, 16), fingerprint = digest({ value, context });
|
|
44
|
+
if (!refresh && task.cache[reference] === fingerprint)
|
|
45
|
+
return { unchanged: true, reference };
|
|
46
|
+
task.cache[reference] = fingerprint;
|
|
47
|
+
await this.save(id, task);
|
|
48
|
+
return { ...value, reference };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async start(paths = [], signal) {
|
|
52
|
+
paths = z.array(relativePath).parse(paths);
|
|
53
|
+
// Pending setup is expected after init; never fabricate knowledge or task state.
|
|
54
|
+
let registry;
|
|
55
|
+
try {
|
|
56
|
+
registry = await this.store.read();
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.code !== 'ENOENT')
|
|
60
|
+
throw e;
|
|
61
|
+
try {
|
|
62
|
+
await this.store.safe('.common-ground/local/bootstrap.json');
|
|
63
|
+
}
|
|
64
|
+
catch (setupError) {
|
|
65
|
+
if (setupError.code !== 'ENOENT')
|
|
66
|
+
throw setupError;
|
|
67
|
+
return { state: 'not-initialized', next: 'Run cground init from the repository root, then review its bootstrap proposal with the developer. Continue the main task from source; no task was started, so do not assess, propose facts or finish.' };
|
|
68
|
+
}
|
|
69
|
+
return { state: 'bootstrap-required', proposalPath: '.common-ground/local/bootstrap.json', next: bootstrapNext };
|
|
70
|
+
}
|
|
71
|
+
const taskId = randomUUID();
|
|
72
|
+
const owners = this.store.chapters(registry).filter(({ chapter }) => paths.some(p => chapter.paths.some(s => overlaps(p, s))));
|
|
73
|
+
const matches = paths.length ? undefined : await ownershipMap(this.store, { signal, limit: 5 });
|
|
74
|
+
const routes = matches ? { items: matches.items.map(({ chapterId, title }) => ({ chapterId, title })), total: matches.total, nextCursor: matches.nextCursor } :
|
|
75
|
+
page(owners.map(({ key, chapter }) => ({ chapterId: key, title: chapter.title })), undefined, 5);
|
|
76
|
+
await this.store.lock(() => this.save(taskId, { phase: 'active', paths: [...new Set(paths)].sort(), cache: {}, changes: {}, pending: [], drafts: [] }));
|
|
77
|
+
const emptyChapters = this.store.chapters(registry).filter(({ chapter }) => !chapter.facts.length).map(({ key }) => key);
|
|
78
|
+
return { taskId, ...(emptyChapters.length ? { setup: { state: 'facts-required', boundariesApproved: true, emptyChapters, next: { action: 'review-facts', approvalRequired: true, schema: 'cground schema seed-batch' } } } : {}), routes: { items: routes.items, total: routes.total, hasMore: routes.nextCursor !== null }, next: 'Read relevant chapters and source. Use read_knowledge owners for more routes. Finish the developer task first; batch additions at finish.' };
|
|
79
|
+
}
|
|
80
|
+
async assess(id, paths, cursor, limit, refresh = false) {
|
|
81
|
+
paths = [...new Set(z.array(relativePath).parse(paths))].sort();
|
|
82
|
+
const registry = await this.store.read();
|
|
83
|
+
const impact = await changedFacts(this.store, registry, paths);
|
|
84
|
+
const affected = impact.changed.map(item => item.entry);
|
|
85
|
+
const byChapter = new Map();
|
|
86
|
+
for (const entry of affected) {
|
|
87
|
+
const ids = byChapter.get(entry.chapterId) ?? [];
|
|
88
|
+
ids.push(entry.fact.id);
|
|
89
|
+
byChapter.set(entry.chapterId, ids);
|
|
90
|
+
}
|
|
91
|
+
const keys = [...new Set([...byChapter].flatMap(([key, ids]) => this.store.related(registry, key, ids)))].sort();
|
|
92
|
+
const files = keys.length ? await this.store.reviewFiles(registry, keys, paths) : { sourceFiles: [], documentFiles: await reviewDocuments(this.store, paths, 'focused') };
|
|
93
|
+
const entries = [...keys.map(chapterId => ({ kind: 'chapter', chapterId, revision: this.store.chapter(registry, chapterId).revision })),
|
|
94
|
+
...files.sourceFiles.map(path => ({ kind: 'source', path })), ...files.documentFiles.map(path => ({ kind: 'document', path }))];
|
|
95
|
+
await this.store.lock(async () => { const task = await this.task(id); if (task.phase !== 'active')
|
|
96
|
+
throw new Error('Task is finished; start a new task for more code work.'); task.paths = paths; await this.save(id, task); });
|
|
97
|
+
const value = { state: keys.length ? 'review-required' : 'no-fact-review', affectedFactCount: affected.length, ...page(entries, cursor, limit, digest(registry)) };
|
|
98
|
+
// Recompute live source and documentation state; unchanged status labels are not fingerprints.
|
|
99
|
+
let context;
|
|
100
|
+
try {
|
|
101
|
+
context = { registry, chapters: await this.store.context(registry, keys), files: await this.store.fileSnapshots([...files.sourceFiles, ...files.documentFiles, ...paths]) };
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return value;
|
|
105
|
+
} // Missing evidence still needs attention; do not reuse a cached result.
|
|
106
|
+
return this.reuse(id, { assess: paths, cursor, limit }, value, context, refresh);
|
|
107
|
+
}
|
|
108
|
+
async prepare(input) {
|
|
109
|
+
const patch = Patch.parse(input), task = patch.taskId ? await this.task(patch.taskId) : undefined;
|
|
110
|
+
if (task && task.phase !== 'active')
|
|
111
|
+
throw new Error('Task is finished; start a new task before maintenance.');
|
|
112
|
+
const registry = await this.store.read();
|
|
113
|
+
const reviews = patch.reviews.map(({ reviewedAllFacts, replacements, removeFactIds, ...review }) => {
|
|
114
|
+
const old = this.store.chapter(registry, review.chapterId);
|
|
115
|
+
if (old.revision !== review.expectedRevision)
|
|
116
|
+
throw new GroundError('REGISTRY_CONFLICT', 'Chapter revision conflict; reload.', ['expectedRevision'], 'Reload and review the chapter before preparing again.');
|
|
117
|
+
unique([...replacements.map(f => f.id), ...removeFactIds], 'patch fact IDs');
|
|
118
|
+
if ([...replacements.map(f => f.id), ...removeFactIds].some(id => !old.facts.some(f => f.id === id)))
|
|
119
|
+
throw new Error('Patches only edit existing facts; queue additions with propose_facts.');
|
|
120
|
+
const facts = old.facts.filter(f => !removeFactIds.includes(f.id)).map(f => replacements.find(r => r.id === f.id) ?? f);
|
|
121
|
+
return { ...review, facts, reviewedFactIds: old.facts.map(f => f.id), invalidatedFactIds: old.facts.filter(f => !same(f, facts.find(n => n.id === f.id))).map(f => f.id) };
|
|
122
|
+
});
|
|
123
|
+
const { taskId, ...rest } = patch;
|
|
124
|
+
const result = await this.store.prepare({ ...rest, reviews });
|
|
125
|
+
if (!result.noop && taskId)
|
|
126
|
+
await this.store.lock(async () => {
|
|
127
|
+
const current = await this.task(taskId);
|
|
128
|
+
if (current.phase !== 'active')
|
|
129
|
+
throw new Error('Task finished during preparation; prepare in a new task.');
|
|
130
|
+
const file = `local/${result.proposalId}.json`;
|
|
131
|
+
await this.store.safe(`.common-ground/${file}`);
|
|
132
|
+
const proposal = JSON.parse(await fs.readFile(this.store.file(file), 'utf8'));
|
|
133
|
+
// Persist pending first. If interrupted, finish reports attention instead of false silence.
|
|
134
|
+
current.pending.push(result.proposalId);
|
|
135
|
+
await this.save(taskId, current);
|
|
136
|
+
await this.store.atomic(file, { ...proposal, taskId });
|
|
137
|
+
});
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async draftFingerprint(registry, chapterId, fact) {
|
|
141
|
+
const candidate = structuredClone(registry), chapter = this.store.chapter(candidate, chapterId);
|
|
142
|
+
if (chapter.facts.some(f => f.id === fact.id))
|
|
143
|
+
throw new Error(`Fact already exists: ${chapterId}/${fact.id}`);
|
|
144
|
+
chapter.facts.push(fact);
|
|
145
|
+
this.store.validateRegistry(candidate);
|
|
146
|
+
await this.store.validateFacts({ paths: chapter.paths, facts: [fact] });
|
|
147
|
+
const dependencies = this.store.upstreamFacts(candidate, `${chapterId}/${fact.id}`);
|
|
148
|
+
const sources = await this.store.snapshot({ paths: chapter.paths, facts: [fact] });
|
|
149
|
+
const upstream = [];
|
|
150
|
+
for (const key of dependencies) {
|
|
151
|
+
const dep = this.store.fact(candidate, key);
|
|
152
|
+
upstream.push({ key, fact: dep.fact, sources: await this.store.snapshot({ paths: dep.chapter.paths, facts: [dep.fact] }) });
|
|
153
|
+
}
|
|
154
|
+
return digest({ chapter: this.store.chapter(registry, chapterId), fact, sources, upstream });
|
|
155
|
+
}
|
|
156
|
+
async propose(id, chapterId, values) {
|
|
157
|
+
const facts = z.array(Fact).min(1).parse(values);
|
|
158
|
+
unique(facts.map(f => f.id), 'proposed fact IDs');
|
|
159
|
+
return this.store.lock(async () => {
|
|
160
|
+
const task = await this.task(id);
|
|
161
|
+
if (task.phase !== 'active')
|
|
162
|
+
throw new Error('Task is finished. Start a new task to propose or reverify additions.');
|
|
163
|
+
const registry = await this.store.read(), drafts = [...task.drafts];
|
|
164
|
+
for (const fact of facts) {
|
|
165
|
+
const draft = { chapterId, fact, fingerprint: await this.draftFingerprint(registry, chapterId, fact) };
|
|
166
|
+
const index = drafts.findIndex(d => d.chapterId === chapterId && d.fact.id === fact.id);
|
|
167
|
+
if (index < 0)
|
|
168
|
+
drafts.push(draft);
|
|
169
|
+
else
|
|
170
|
+
drafts[index] = draft;
|
|
171
|
+
}
|
|
172
|
+
task.drafts = drafts;
|
|
173
|
+
await this.save(id, task);
|
|
174
|
+
return { queued: facts.length, total: task.drafts.length, sharedKnowledgeChanged: false };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async draftStates(task, registry) {
|
|
178
|
+
const states = [];
|
|
179
|
+
for (const draft of task.drafts) {
|
|
180
|
+
let stale = false;
|
|
181
|
+
try {
|
|
182
|
+
stale = draft.fingerprint !== await this.draftFingerprint(registry, draft.chapterId, draft.fact);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
stale = true;
|
|
186
|
+
}
|
|
187
|
+
states.push({ kind: 'new-fact', factId: `${draft.chapterId}/${draft.fact.id}`, statement: draft.fact.statement, evidencePaths: [...new Set(draft.fact.evidence.map(e => e.path))], stale, recommendation: stale ? 'reverify' : 'present-for-approval' });
|
|
188
|
+
}
|
|
189
|
+
return states;
|
|
190
|
+
}
|
|
191
|
+
async finish(id, cursor, limit) {
|
|
192
|
+
return this.store.lock(async () => {
|
|
193
|
+
const task = await this.task(id), registry = await this.store.read();
|
|
194
|
+
const additions = await this.draftStates(task, registry);
|
|
195
|
+
task.phase = 'awaiting-approval';
|
|
196
|
+
await this.save(id, task);
|
|
197
|
+
const changes = [];
|
|
198
|
+
const states = new Map();
|
|
199
|
+
for (const [factId, statement] of Object.entries(task.changes)) {
|
|
200
|
+
let current = null;
|
|
201
|
+
try {
|
|
202
|
+
current = this.store.fact(registry, factId).fact;
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
const detail = task.changeDetails?.[factId];
|
|
206
|
+
const changedSinceUpdate = detail ? !same(current, detail.after) : (current?.statement ?? '(removed)') !== statement;
|
|
207
|
+
const delta = detail ? factReview(detail.before, detail.after) : undefined;
|
|
208
|
+
if (detail && !delta && !changedSinceUpdate)
|
|
209
|
+
continue; // A correction reverted in this task has no net change.
|
|
210
|
+
let evidenceValid = false;
|
|
211
|
+
if (detail)
|
|
212
|
+
try {
|
|
213
|
+
const key = factId.slice(0, factId.lastIndexOf('/'));
|
|
214
|
+
await this.store.validateFacts({ paths: this.store.chapter(registry, key).paths, facts: current ? [current] : [] });
|
|
215
|
+
if (!states.has(key))
|
|
216
|
+
states.set(key, (await this.store.status(key, registry)).status);
|
|
217
|
+
evidenceValid = current === null && detail.after === null || states.get(key) === 'evidence-unchanged';
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
220
|
+
changes.push({ kind: 'updated-fact', factId, ...(!delta || !delta.fields.statement ? { statement } : {}), ...(delta ?? {}),
|
|
221
|
+
reason: detail?.reason ?? 'Older task receipt has no before/after detail; use cground review.',
|
|
222
|
+
changedSinceUpdate, evidenceStatus: evidenceValid ? 'mechanically-current' : 'needs-review',
|
|
223
|
+
recommendation: changedSinceUpdate || !evidenceValid ? 'reverify' : 'keep-verified-correction', approvalRequired: false });
|
|
224
|
+
}
|
|
225
|
+
const entries = [...changes, ...additions,
|
|
226
|
+
...task.pending.map(proposalId => ({ kind: 'uncommitted-review', proposalId }))];
|
|
227
|
+
const attention = task.pending.length || additions.some(a => a.stale) || changes.some(c => c.recommendation === 'reverify');
|
|
228
|
+
const reviewPrompt = attention
|
|
229
|
+
? 'Resolve pending reviews or changed evidence before sharing these facts with the team.'
|
|
230
|
+
: additions.length
|
|
231
|
+
? 'Ready to make the following facts available to the team? Review the proposed facts and evidence before approving admission.'
|
|
232
|
+
: 'Summarize applied corrections: before → after, reason, source links, and your recommendation. The developer does not need to open or edit JSON.';
|
|
233
|
+
return { notification: attention ? 'attention' : additions.length ? 'approval-required' : entries.length ? 'summary' : 'none',
|
|
234
|
+
...page(entries, cursor, limit, '', 12000), ...(entries.length ? { reviewPrompt, reviewFormat: 'Present changed items only, grouped by pillar/chapter. Include before → after, why, source links, and keep/approve/reverify recommendation. Corrections are applied; additions still await explicit approval. Mechanical checks do not prove semantic truth; state uncertainty.' } : {}) };
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async proposals(id, cursor, limit) {
|
|
238
|
+
const task = await this.task(id);
|
|
239
|
+
return page(task.drafts.map(({ fingerprint, ...draft }) => draft), cursor, limit, '', 12000);
|
|
240
|
+
}
|
|
241
|
+
async drop(id, keys) {
|
|
242
|
+
if (!keys.length)
|
|
243
|
+
throw new Error('Supply fully qualified fact IDs to discard.');
|
|
244
|
+
return this.store.lock(async () => {
|
|
245
|
+
const task = await this.task(id);
|
|
246
|
+
if (keys.some(key => !task.drafts.some(d => `${d.chapterId}/${d.fact.id}` === key)))
|
|
247
|
+
throw new Error('Unknown queued fact.');
|
|
248
|
+
task.drafts = task.drafts.filter(d => !keys.includes(`${d.chapterId}/${d.fact.id}`));
|
|
249
|
+
await this.save(id, task);
|
|
250
|
+
return { remaining: task.drafts.length, sharedKnowledgeChanged: false };
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async admissionPlan(id) {
|
|
254
|
+
const task = await this.task(id), registry = await this.store.read(), candidate = structuredClone(registry);
|
|
255
|
+
if (task.phase !== 'awaiting-approval')
|
|
256
|
+
throw new Error('Finish the developer task before admitting new facts.');
|
|
257
|
+
if (!task.drafts.length)
|
|
258
|
+
throw new Error('No queued facts.');
|
|
259
|
+
if ((await this.draftStates(task, registry)).some(d => d.stale))
|
|
260
|
+
throw new Error('Queued evidence or knowledge changed; reverify and propose in a new task.');
|
|
261
|
+
for (const d of task.drafts)
|
|
262
|
+
this.store.chapter(candidate, d.chapterId).facts.push(d.fact);
|
|
263
|
+
this.store.validateRegistry(candidate);
|
|
264
|
+
const keys = [...new Set(task.drafts.flatMap(d => this.store.related(candidate, d.chapterId)))].sort();
|
|
265
|
+
return { task, registry, candidate, keys };
|
|
266
|
+
}
|
|
267
|
+
async proposalReview(id, cursor, limit) {
|
|
268
|
+
const { task, registry, candidate, keys } = await this.admissionPlan(id);
|
|
269
|
+
const files = await this.store.reviewFiles(registry, keys, task.paths, candidate);
|
|
270
|
+
return page([...keys.map(chapterId => ({ kind: 'chapter', chapterId, expectedRevision: this.store.chapter(registry, chapterId).revision })),
|
|
271
|
+
...files.sourceFiles.map(path => ({ kind: 'source', path })), ...files.documentFiles.map(path => ({ kind: 'document', path }))], cursor, limit, digest({ candidate, files }));
|
|
272
|
+
}
|
|
273
|
+
async accept(id, input) {
|
|
274
|
+
const review = Admission.parse(input);
|
|
275
|
+
return this.store.lock(async () => {
|
|
276
|
+
const { task, registry, candidate, keys } = await this.admissionPlan(id);
|
|
277
|
+
if (task.pending.length)
|
|
278
|
+
throw new Error('Resolve uncommitted maintenance before admitting new facts.');
|
|
279
|
+
unique(review.reviews.map(r => r.chapterId), 'chapter reviews');
|
|
280
|
+
if (!same(keys, review.reviews.map(r => r.chapterId).sort()))
|
|
281
|
+
throw new Error(`Review every linked chapter: ${keys.join(', ')}`);
|
|
282
|
+
for (const item of review.reviews)
|
|
283
|
+
if (this.store.chapter(registry, item.chapterId).revision !== item.expectedRevision)
|
|
284
|
+
throw new GroundError('REGISTRY_CONFLICT', 'Chapter revision conflict; reload.', ['expectedRevision'], 'Reload and review the chapter before preparing again.');
|
|
285
|
+
const files = await this.store.reviewFiles(registry, keys, task.paths, candidate);
|
|
286
|
+
for (const kind of ['sourceFiles', 'documentFiles']) {
|
|
287
|
+
unique(review.verification[kind], 'verified files');
|
|
288
|
+
for (const file of files[kind])
|
|
289
|
+
if (!review.verification[kind].includes(file))
|
|
290
|
+
throw new Error(`Session verification required for ${kind}: ${file}`);
|
|
291
|
+
}
|
|
292
|
+
const snapshots = {};
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
const chapter = this.store.chapter(candidate, key);
|
|
295
|
+
await this.store.validateFacts(chapter);
|
|
296
|
+
snapshots[key] = await this.store.snapshot(chapter);
|
|
297
|
+
}
|
|
298
|
+
await this.store.fileSnapshots([...review.verification.sourceFiles, ...review.verification.documentFiles]);
|
|
299
|
+
const changed = [...new Set(task.drafts.map(d => d.chapterId))];
|
|
300
|
+
for (const key of changed) {
|
|
301
|
+
const c = this.store.chapter(candidate, key);
|
|
302
|
+
c.revision++;
|
|
303
|
+
c.sources = snapshots[key];
|
|
304
|
+
c.dependencyFingerprints = this.store.dependencyFingerprints(candidate, key);
|
|
305
|
+
}
|
|
306
|
+
if (!same(await this.store.read(), registry) || (await this.draftStates(task, registry)).some(d => d.stale))
|
|
307
|
+
throw new Error('Source or knowledge changed during admission; reverify.');
|
|
308
|
+
// Validate the entire batch before the one shared registry write.
|
|
309
|
+
await this.store.persist(candidate);
|
|
310
|
+
const admitted = task.drafts.length;
|
|
311
|
+
task.drafts = [];
|
|
312
|
+
await this.save(id, task);
|
|
313
|
+
await this.store.markReviewed(candidate, keys, snapshots);
|
|
314
|
+
return { admitted, changedChapters: changed };
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Architecture and beta boundaries
|
|
2
|
+
|
|
3
|
+
Common Ground is an open-source framework combining a repository knowledge model, maintenance rules, and a local implementation developers can adapt and extend. The CLI and stdio MCP server expose that framework to coding agents. Integrations can use those interfaces and the published JSON schemas; implementation changes live in the TypeScript modules below.
|
|
4
|
+
|
|
5
|
+
The supported customization surface is repository definitions and supplemental guidance, plus CLI/MCP integrations. Custom discovery, validators, record fields, storage backends, and tools require source changes. There is no plugin loader, stable public SDK, lifecycle-hook API, or configurable discovery-provider interface in this beta. Internal module exports are implementation details rather than a promised extension contract.
|
|
6
|
+
|
|
7
|
+
- `src/access.ts`: stateless bounded lookup with separate registry-based ownership hints, optional fact-level verification, and source-sensitive change assessment with opt-in complete review packages.
|
|
8
|
+
- `src/matching.ts`: shared technical tokenization, exact version and short-acronym boundaries, corpus-based term weights and direct-path recognition.
|
|
9
|
+
- `src/source-search.ts`: separate bounded live-source search with explicit paths, lexical line excerpts and no knowledge writes.
|
|
10
|
+
- `src/review.ts`: read-only content deltas for pillars/chapters/facts and compact task correction presentation; no second shared knowledge store.
|
|
11
|
+
- `src/operations.ts`: shared CLI/MCP execution and validation, per-operation schemas and approval declarations.
|
|
12
|
+
- `src/commands.ts`: dependency-free CLI syntax/help metadata and strict built-in argument parsing.
|
|
13
|
+
- `src/retrieval.ts`: transport-independent navigation and task-scoped retrieval, also re-exported by the server for compatibility.
|
|
14
|
+
- `src/maintenance.ts`: shared CLI/MCP checks, stale summaries and scoped cleanup handoffs.
|
|
15
|
+
- `src/export.ts`: complete deterministic Markdown rendering and atomic refresh of the ignored local reference.
|
|
16
|
+
- `src/model.ts`: strict schema v2 pillar, chapter, fact, and multi-chapter review records; schema v1 migration input.
|
|
17
|
+
- `src/store.ts`: safe paths, evidence and ownership checks, dependency traversal, freshness, migration, and atomic review transactions.
|
|
18
|
+
- `src/workflow.ts`: task-local context reuse, compact patches, deferred fact proposals, and reviewed batch admission.
|
|
19
|
+
- `src/hooks.ts`: default advisory Git hook installation, staged-source checks, and checkout-local notification preferences.
|
|
20
|
+
- `src/init.ts`: managed repository instructions/MCP configuration and bootstrap orchestration.
|
|
21
|
+
- `src/discovery.ts`: bounded static manifest/source-path discovery, technology signals and proposed project ownership.
|
|
22
|
+
- `src/server.ts`: six default stdio MCP tools (fourteen in the compatibility profile), paginated navigation, evidence retrieval, and Unicode-aware keyword search.
|
|
23
|
+
- `src/navigation.ts`: derived ownership routing, pillar graph, Start Here responses, read-only scoped validation, and tidy plans including whole-registry review.
|
|
24
|
+
- `src/review-files.ts`: bounded discovery of local, sibling, child, and referenced review documentation.
|
|
25
|
+
- `src/guidance.ts`: short entry instructions, invariant policy and managed Start Here guide.
|
|
26
|
+
- `src/paging.ts`: shared pagination and cursor validation.
|
|
27
|
+
- `src/errors.ts`: stable CLI JSON error envelopes and typed domain errors.
|
|
28
|
+
- `src/cli.ts`: shell-to-operation adapter, concise terminal output, and lazy MCP entry point.
|
|
29
|
+
|
|
30
|
+
`.common-ground/knowledge.json` is a Git-tracked registry. Pillars contain chapter definitions; chapters index facts and define authoring boundaries. Facts own exact evidence, source scopes, and directed fact-to-fact dependencies. One atomic file permits all affected chapter revisions to publish together. `.common-ground/local/` contains a generated `knowledge.md` view of the complete registry, ignored proposals, review snapshots, a migration backup, tidy request receipts, task receipts/context fingerprints, queued fact drafts, and a writer lock. `.common-ground/START_HERE.md` and `.common-ground/POLICY.md` are tracked guidance. Ownership maps and pillar graphs are derived from the registry rather than duplicated in a second data model.
|
|
31
|
+
|
|
32
|
+
Storage has no fixed fact-count ceiling. Pillar and chapter indexes and chapter fact reads are paginated (10 by default, up to 20 per response). Stateless lookup reads only the registry unless verification is requested. Assessment hashes only touched portions of candidate source scopes before expanding review. Request-scoped hash reuse deduplicates read work; publication checks never reuse those caches. The compact profile can batch complete facts with evidence, using a 12,000-character soft page budget (a single oversized record stays intact). The full profile also supports individual fact reads. Keyword search returns at most 20 summaries. Pagination rejects changed index/fact content. A full chapter review must traverse all its fact pages.
|
|
33
|
+
|
|
34
|
+
The current storage engine still parses the full registry internally, and source hashing is synchronous work per tool request. The beta has no database index, background watcher, vector search, or incremental validator. The pre-commit hook invokes an advisory check against a temporary export of the Git index; it never rewrites knowledge or blocks commits. Existing hook managers require adding cground hook check to their entry point. Response pagination limits agent context, not internal compute. Very large knowledge stores need performance evaluation and likely indexed/per-chapter storage before a production claim.
|
|
35
|
+
|
|
36
|
+
Lookup preserves version tokens, downweights terms shared across the stored corpus and prioritizes direct evidence paths. Compact results contain one statement, source-path list, relevance label and freshness result per fact; `verbose:true` adds matching diagnostics. Weak lexical coverage leads with “No direct answer found,” then bounded registry navigation and a separate, unexecuted source-search suggestion. The `source-search` operation requires explicit paths and labels results `live-source-evidence`. It reads at most 2,000 entries, 200 files, 1 MiB per file and 4 MiB total, returning up to five line excerpts. It skips symlinks, binary/non-UTF-8 files, generated/dependency paths and hidden configuration except `.github`; exclusions and truncation are explicit. It neither accesses the registry nor runs project commands. See [the setup guide](../SETUP.md#search-live-source-when-knowledge-is-thin).
|
|
37
|
+
|
|
38
|
+
Dependency reviews start from selected fact IDs (or all facts in a chapter), traverse fact dependencies and reverse dependents transitively, and group the result into chapter reviews. Every linked chapter must be reviewed, but only changed facts and chapters are rewritten. Explicit maintenance can correct an existing false claim, merge duplicates, remove superseded content, or tighten narrative without requiring source drift. Routine maintenance is confined to affected chapters; developer-requested tidy receipts authorize a bounded cleanup scope. A dense graph can still produce expensive reviews. Cross-pillar relationships are supported; missing undeclared dependencies cannot be inferred reliably. Shared chapter membership alone does not propagate dependency impact.
|
|
39
|
+
|
|
40
|
+
Discovery examines at most 1,500 filesystem entries breadth-first, reads at most 256 KiB per manifest and 2 MiB of accepted manifest content, and returns up to 50 project candidates with 30 file/directory path hints each. It recognizes common web/mobile/native/backend frameworks, workspace markers and CI layouts. Signals are grouped by project root and broad responsibility; framework labels never become facts automatically. Native subtrees of React Native/Expo/Flutter projects are grouped with the parent. Ownership hints collapse homogeneous fully scanned subtrees into directory suggestions, while mixed, excluded, nested-owner or truncated areas retain narrower hints. Responsibility rationales and delivery coverage prompts help the agent refine the map before authorized initial publication. All sampling and malformed/skipped manifests are reported. Developers approve an agent-refined map; discovery does not prove exhaustive coverage. See [the discovery catalog](discovery.md). Fact scope scans remain separately limited to 10,000 entries. Freshness hashing streams regular files without a per-file size cap; quotation evidence retains a 2,000,000-byte limit. Supporting evidence can cross chapter ownership and remains part of freshness and task impact. Bootstrap and empty-chapter batches support no-write preflight with file counts, then one approval-directed atomic publication guarded by a content token.
|
|
41
|
+
|
|
42
|
+
Semantic entailment, approval UI, automatic command capture, remote sync, scope migration, and native Windows testing remain out of scope. `doctor` checks expected file presence, registry validity and managed guidance against installed templates; it is not a complete MCP-host diagnostic.
|
|
43
|
+
|
|
44
|
+
Snapshots are optimistic checks, not OS-level filesystem snapshots. Source changes can occur after the final check. Registry writes are serialized and atomically renamed on one filesystem. If a process crashes holding a lock, confirm no writer remains before deleting `.common-ground/local/write.lock`.
|
|
45
|
+
|
|
46
|
+
Avoid secrets in records. .env paths are excluded; comprehensive secret scanning is not implemented. Common Ground sends nothing to a remote service, but an agent host may transmit retrieved results to its model provider.
|
|
47
|
+
|
|
48
|
+
Update requests now require session verification declarations for evidence source and discovered documentation, including no-op reviews. Commit rejects subsequent source/documentation drift. These declarations are not proof of external agent reading, and semantic correction remains the agent's responsibility. Point-in-time state, unverified claims, and debugging narratives are forbidden by policy, not by a semantic classifier. Legacy seed/admit operations still enforce exact evidence and developer direction; their session-reading and cleanup rules are agent responsibilities.
|
|
49
|
+
|
|
50
|
+
Documentation discovery scans at most 10,000 scoped entries and follows at most 1,000 local documents. Root-level changes do not recursively scan the entire monorepo. The checklist follows Markdown document links, not every programming-language import or documentation reference syntax; agents must follow other relevant references themselves.
|
|
51
|
+
|
|
52
|
+
Compact patches declare `reviewedAllFacts: true` against a chapter revision and reconstruct unchanged records server-side before invoking the existing validator. They do not weaken whole-chapter review. Task receipts track only committed corrections from that task; pending failed transactions produce an attention result at finish. A crash after a shared write but before its local receipt is saved can require manual reconciliation. Task state remains local and disposable after review, not shared history.
|
|
53
|
+
|
|
54
|
+
New fact proposals are local and excluded from authoritative retrieval. They snapshot their chapter, source scopes and admitted upstream dependencies. Finished tasks can be admitted through explicit approval CLI or MCP operations, with a complete linked-chapter review and source/documentation declarations. Stale drafts require fresh verification and a new proposal batch. The batch writes atomically across chapters. Dependencies on other pending drafts are not supported. Legacy seed/admit commands remain available for developer-directed bootstrap/operator work, so finish/approval checks are a workflow boundary, not a sandbox against filesystem access.
|
|
55
|
+
|
|
56
|
+
Response reuse is task-scoped and recomputes live fingerprints; it does not cache Git branch/submodule state, prove source reading, or survive context loss semantically. The caller must refresh or start a new task after losing earlier responses. Internal full-registry parsing and source hashing remain; byte savings measure transferred context, not all CPU, latency, or billed tokens. See `npm run measure:context` and the regression fixtures in `test/workflow.test.mjs`.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
The Markdown export contains shared records only, including evidence, dependency links, revisions and stored fingerprints. Init creates a bootstrap placeholder when the registry is absent. CLI validate/tidy and successful shared writes refresh the export; pure navigation/MCP tidy previews remain read-only. Export output is deterministic and compared before atomic replacement, so unchanged content preserves file modification time. CLI refreshes use the writer lock; shared-write refreshes run within their existing transaction. Failure to refresh this disposable view after a successful knowledge write is reported on stderr without undoing or misreporting that write. No local drafts or review receipts are rendered.
|
|
60
|
+
|
|
61
|
+
Task-local correction receipts retain only the before/after records for facts actually changed, plus a reason, so finish can show a net delta and detect later evidence-only edits. They are ignored local state, not a second shared history. Git review compares the registry with HEAD or the index without writing files; it never implies approval or semantic correctness.
|
|
62
|
+
|
|
63
|
+
CLI JSON errors use `{error: {code, message, fields, recovery}}` on stderr with a nonzero exit status. Codes include `INPUT_NOT_FOUND`, `INPUT_UNREADABLE`, `INVALID_JSON`, `INVALID_OPTION_VALUE`, `UNKNOWN_COMMAND`, `REGISTRY_INVALID`, `REGISTRY_MIGRATION_REQUIRED`, `REGISTRY_CONFLICT`, and `SOURCE_CONFLICT`. Missing registries still return onboarding guidance where supported; corrupt registries never masquerade as missing setup. `INIT_INCOMPLETE` additionally supplies `details` with completed, skipped, failed and pending setup steps and the underlying error. MCP operation failures preserve these initialization details under `diagnostic`. Advisory hook permission failures return setup warnings without aborting initialization; `skipHook:true` omits hook installation. `fields` identifies the input filename, option, registry, or changed reviewed paths when known. Unclassified failures retain `OPERATION_FAILED`; callers should tolerate unfamiliar codes. Doctor remains a diagnostic result (`valid:false` and registry details), rather than a thrown operation error.
|
|
64
|
+
|
|
65
|
+
Guidance refresh reports `changedFiles` and `unchangedFiles` without touching the registry, MCP configuration or hooks. A completed refresh and healthy doctor return `next:null`; outdated guidance still names the refresh command. Bootstrap publication recommends lookup, not mandatory task bookkeeping.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 0.4.0 beta audit
|
|
2
|
+
|
|
3
|
+
The audit covered CLI parsing and help, CLI/MCP parity, onboarding, dependencies and startup, knowledge storage and review transactions, source and documentation discovery, pre-commit checks, Markdown export, tests, packaging, and developer documentation. The changes focus on reducing duplicated execution paths and making existing capabilities easier to use.
|
|
4
|
+
|
|
5
|
+
## Findings and changes
|
|
6
|
+
|
|
7
|
+
| Finding | Change |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `init --help` could initialize a repository; most commands lacked contextual help | Handle help before loading or running operations. Every command gets usage, options, and examples, including nested hook/task commands. Regression tests check every command against an empty directory. |
|
|
10
|
+
| Options were removed from a global argument array regardless of which command used them | Use Node's built-in `util.parseArgs`, validate options per command, and reject incorrect arity and values before execution. Support `-h`, `-V`, `--name=value`, and `--` consistently. |
|
|
11
|
+
| CLI and MCP duplicated domain dispatch and validation | CLI arguments now adapt to the same structured operation handlers used by MCP. A two-way parity test checks the complete command catalog. |
|
|
12
|
+
| The operations module imported the MCP server, which imported operations | Extract retrieval into a transport-independent module; only `serve` loads the MCP runtime. Server exports remain compatible. |
|
|
13
|
+
| Help/version loaded Zod, the knowledge engine, and MCP | Keep early command handling independent of those modules. Test a help/version installation containing only the entry point, command metadata, and package version reader. |
|
|
14
|
+
| Check/validate schemas repeated the same behavior | Make both names reference the same operation definition. |
|
|
15
|
+
| Check output was verbose for terminal users | Return a concise terminal summary; preserve JSON for pipes and `--json`, with diagnostics on stderr and no prompts in pipelines. |
|
|
16
|
+
| Markdown export existed through MCP but had no direct CLI command | Add `cground export`, using the existing operation. |
|
|
17
|
+
| Muted pre-commit reminders still exported and hashed the index | Return after reading the notification preference when muted; there is no check result to consume or persist. |
|
|
18
|
+
| Getting started required scanning implementation and operator details | Shorten the README around install → init → review → check, then link the detailed workflow and contracts. |
|
|
19
|
+
|
|
20
|
+
## What stays, and why
|
|
21
|
+
|
|
22
|
+
- **One shared JSON registry:** already a small, understandable persistence model. No database, watcher, queue service, cloud dependency, or plugin runtime was added.
|
|
23
|
+
- **Atomic writes, writer lock, revision/source checks, whole-chapter review, and approval declarations:** these protect shared knowledge from partial writes, stale evidence, unreviewed edits, and accidental growth. Removing them would weaken the framework's core behavior.
|
|
24
|
+
- **Task-local receipts, drafts, and bounded context reuse:** these support deferred approval, accurate completion reports, and smaller repeated reads. They remain Git-ignored. No new cache layer was introduced.
|
|
25
|
+
- **Staged Git export:** more work than checking the working tree, but needed to catch a stale staged change even when unstaged files already contain a fix. The existing filter and symlink protections stay. This can be expensive in a large repository.
|
|
26
|
+
- **Both MCP profiles and existing CLI names:** preserving integrations is more useful than deleting aliases simply to reduce command count. The everyday commands appear first in help; all advanced commands remain discoverable.
|
|
27
|
+
- **MCP SDK, Zod, and JSONC parser:** each has a concrete role in protocol compatibility, shared schemas, and preserving user configuration. No new runtime dependency was added. The lockfile contains 95 production packages including transitive dependencies; three direct dependencies does not mean three installed packages.
|
|
28
|
+
|
|
29
|
+
## Measurements and checks
|
|
30
|
+
|
|
31
|
+
Nine local runs of each help command, immediately before upgrading the installed package, measured median startup of **188.0 ms for installed 0.3.0-beta.1** and **30.5 ms for the new CLI**. These are warm local wall-clock measurements, not cross-machine or cold-start guarantees. They measure help startup, not validation throughput.
|
|
32
|
+
|
|
33
|
+
Validation covers all-command help with no repository writes, strict arguments, CLI/MCP parity, actual stdio MCP calls, approval gates, synthetic source/dependency changes, source/revision conflicts, staged versus unstaged hook behavior, deterministic export, schema generation, demo and package installation. The behavior suite contains 142 passing tests, including checks of both MCP profiles. Release packaging also runs the synthetic demo and regenerates schemas.
|
|
34
|
+
|
|
35
|
+
This is a maintainability and developer-experience audit, not a claim of exhaustive security verification or production-scale performance. Native Windows and VS Code GUI acceptance remain unverified. Very large registries still require profiling; the core parses the registry and hashes source per request. Exact evidence and hashes establish mechanical consistency, not the truth of natural-language claims.
|
|
36
|
+
|
|
37
|
+
## CLI conventions consulted
|
|
38
|
+
|
|
39
|
+
- [npm help](https://docs.npmjs.com/cli/v11/commands/npm-help/): discover command documentation from the terminal.
|
|
40
|
+
- [Commander](https://github.com/tj/commander.js#automated-help): contextual help, explicit argument syntax, and rejection of unknown options. Common Ground uses Node's parser rather than adding a CLI dependency.
|
|
41
|
+
- [Prettier CLI](https://prettier.io/docs/cli#--check): concise check summaries, affected-item lists, and documented exit behavior.
|
|
42
|
+
- [Node util.parseArgs](https://nodejs.org/api/util.html#utilparseargsconfig): standard strict parsing and positional argument support.
|
package/docs/demo.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Synthetic workflow demo
|
|
2
|
+
|
|
3
|
+
Build and pack the source, then install the resulting `nazty_labs-common-ground-0.5.1.tgz` using npm. Copy `examples/demo-monorepo` to a separate directory and run:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
cground init
|
|
7
|
+
cground approve demo-data/plan.json --approve
|
|
8
|
+
cground seed workspace-tooling/overview demo-data/workspace-tooling.json --approve
|
|
9
|
+
cground seed ci-cd/overview demo-data/ci-cd.json --approve
|
|
10
|
+
cground seed web-components/search demo-data/web-components.json --approve
|
|
11
|
+
cground seed web-components/results demo-data/web-results.json --approve
|
|
12
|
+
cground seed java-application/overview demo-data/java-application.json --approve
|
|
13
|
+
cground start "build failed"
|
|
14
|
+
cground owners --path pipelines/build.yml
|
|
15
|
+
cground graph ci-cd
|
|
16
|
+
cground chapters web-components
|
|
17
|
+
cground read web-components/search
|
|
18
|
+
cground fact web-components/search search-minimum
|
|
19
|
+
cground review-plan web-components/search --facts search-minimum
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The supplied plan is synthetic reviewed fixture data. Real initialization requires source review and refined responsibility boundaries. Initial publication needs content approval or explicit developer delegation to save the verified map within scope.
|
|
23
|
+
|
|
24
|
+
Open the directory in VS Code and start Common Ground with MCP: List Servers. Ask Copilot to route a build failure to its owner and inspect the recorded CI → workspace dependency, then select Search and explain its facts. Check that six default tools are available. The original CLI commands below remain supported. Commit source, shared registry, Start Here guide, detailed policy, instructions, and MCP configuration; local metadata remains ignored. No Java build dependency is invented: the fixture does not establish a complete Nx/Java application stack.
|
|
25
|
+
|
|
26
|
+
Change `SEARCH_MIN_LENGTH = 2` to `SEARCH_MIN_LENGTH = 3` in `packages/ui/search.ts`. The Results validation fact depends on the Search threshold fact, so both chapters must be reviewed:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
cground review-plan web-components/search --facts search-minimum
|
|
30
|
+
cground review-checklist web-components/search web-components/results --touched packages/ui/search.ts
|
|
31
|
+
cground prepare demo-data/search-update.json
|
|
32
|
+
cground commit PASTE_RETURNED_PROPOSAL_ID
|
|
33
|
+
cground check
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Before preparing, open the source files, `AGENTS.md`, and `packages/ui/README.md` listed in the request's verification fields. Those fields declare actual reading, not an instruction to skip it. Confirm that the README remains valid even though it needs no edit.
|
|
37
|
+
|
|
38
|
+
The request corrects the Search fact and includes an unchanged full review of Results. Publication increments Search's revision only. Results still uses the shared constant, so its existing assertion remains true. The local review record acknowledges the dependency check without rewriting valid shared knowledge.
|
|
39
|
+
|
|
40
|
+
For an automated version, run `npm run demo` from the Common Ground source directory. It copies the fixture to a temporary directory and prints its location. These fixtures illustrate contracts; they are not a complete runnable Nx/Java application stack.
|
|
41
|
+
|
|
42
|
+
The automated demo also uses a task-scoped compact patch, checks that an unchanged page is reused, and verifies that finish reports only the corrected search fact. For a walkthrough of deferred additions and approval, see [the quiet task workflow](quiet-workflow.md). Run `npm run measure:context` to compare serialized context sizes.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Repository discovery
|
|
2
|
+
|
|
3
|
+
`cground init` uses bounded static discovery to propose an initial responsibility map. `cground scan` returns the same kind of preview without writing knowledge. Once a repository has an approved registry, `init` refreshes managed guidance and configuration while preserving that registry and skipping discovery.
|
|
4
|
+
|
|
5
|
+
Discovery never runs project commands, installs dependencies, imports configuration scripts, invokes a model, seeds facts, or records inferred dependencies. Its observations are navigation hints for the calling agent. The agent reviews code and local READMEs and refines the proposed boundaries. Initial publication needs content approval or explicit developer delegation to save/publish a verified initial map within scope; discovery itself grants no authorization.
|
|
6
|
+
|
|
7
|
+
## Built-in signals
|
|
8
|
+
|
|
9
|
+
| Family | Recognized signals |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Angular | `@angular/core` dependency, `angular.json` project roots/types, Angular Nx executor/builder names |
|
|
12
|
+
| React / Next.js | `react` and `next` dependencies; matching explicit Nx executor names |
|
|
13
|
+
| Vue / Nuxt | `vue` and `nuxt` dependencies, `.vue` source paths; matching Nx executor names |
|
|
14
|
+
| Svelte / SvelteKit / Astro | `svelte`, `@sveltejs/kit`, `astro` dependencies; `.svelte` and `.astro` paths |
|
|
15
|
+
| React Native / Expo | `react-native` or `expo` dependencies, an `expo` object in `app.json`/`app.config.json`, matching Nx executor names |
|
|
16
|
+
| Swift / native Apple | `Package.swift`, Xcode `project.pbxproj`, `.swift` source paths; Swift alone does not imply iOS |
|
|
17
|
+
| Java / Kotlin / Android | Maven `pom.xml`, Gradle build/settings files, `.java`/`.kt` paths, `AndroidManifest.xml`, Android plugin identifiers; Gradle alone does not imply Java |
|
|
18
|
+
| Spring Boot | `org.springframework.boot` in Maven/Gradle manifests |
|
|
19
|
+
| Node / TypeScript backends | `package.json`, JS/TS source paths, `express`, `fastify`, `@nestjs/core`, `typescript` dependencies |
|
|
20
|
+
| Python | `pyproject.toml`, requirements files, `Pipfile`, `setup.py`, `.py` paths; Django, FastAPI and Flask names in dependency manifests |
|
|
21
|
+
| Go / Rust | `go.mod`, `go.work`, `Cargo.toml`, `.go`/`.rs` paths; Cargo workspace tables |
|
|
22
|
+
| .NET | Project/solution filenames (`.csproj`, `.fsproj`, `.vbproj`, `.sln`, `.slnx`) and C#/F# source paths |
|
|
23
|
+
| Ruby / PHP | `Gemfile`, `composer.json`, Ruby/PHP paths; Rails gem and Laravel requirement declarations |
|
|
24
|
+
| Dart / Flutter | `pubspec.yaml`, `.dart` paths, a Flutter key in the manifest |
|
|
25
|
+
| Workspaces / monorepos | npm/Yarn workspaces, pnpm workspace files, Nx project/workspace files, Turborepo, Lerna, Angular workspace projects, Go/Cargo/Gradle workspace markers, conventional `apps/`, `packages/`, `libs/`, `services/`, `projects/` roots |
|
|
26
|
+
| Shared libraries | Explicit Angular/Nx library declarations and conventional `libs/` or `packages/ui`, `packages/components`, `packages/shared` JS/TS paths |
|
|
27
|
+
| CI/CD | GitHub Actions, Azure pipeline filenames and `pipelines/` YAML, GitLab CI, CircleCI, Jenkinsfile |
|
|
28
|
+
|
|
29
|
+
Package signals come from declared dependencies, development dependencies, peer dependencies, and optional dependencies. Merely mentioning React in a README or lockfile, or depending on `@types/react`, does not identify a React application. Source extensions identify language hints; JSX/TSX alone does not prove React. Text-based checks for non-JSON manifests are heuristics, not full parsers or proof of runtime use.
|
|
30
|
+
|
|
31
|
+
These built-ins follow recognizable conventions such as [Angular workspace project definitions](https://angular.dev/reference/configs/workspace-config), [Nx explicit project targets](https://nx.dev/docs/reference/project-configuration), [Swift package manifests](https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html), [Expo configuration](https://docs.expo.dev/versions/latest/config/app/), and [Flutter pubspec files](https://docs.flutter.dev/tools/pubspec). They do not require a particular framework version or execute its tooling.
|
|
32
|
+
|
|
33
|
+
## Responsibilities, not one pillar per technology
|
|
34
|
+
|
|
35
|
+
Detections contain raw signals: a project root, technology labels, supporting paths and counts. Each links to a coarse candidate chapter and explains its rationale. Separate `responsibilityHints` use path conventions to suggest native code, Python interfaces, tests, examples and delivery responsibilities for review. They are alternatives to consider, not additional approved chapters or claims about behavior. A Next.js application can list both Next.js and React in one chapter. A React Native application can include React, Swift and Java/Kotlin signals from its `ios/` and `android/` subtrees without creating independent pillars for those implementation layers.
|
|
36
|
+
|
|
37
|
+
Projects are grouped under broad candidate responsibilities such as web applications, mobile applications, native packages, JVM projects, shared libraries, workspace tooling and CI/CD. Multiple projects in the same family become chapter candidates under one pillar. A repository may need different ownership boundaries, or multiple technology families may belong to one subsystem: the agent must merge or adjust those proposals with the developer. These defaults do not establish semantic independence.
|
|
38
|
+
|
|
39
|
+
Nested manifests and Angular declarations establish project roots. Explicit Nx executor names can distinguish applications with dependencies shared at the workspace root. Nx inferred tasks and arbitrary build scripts are not evaluated. Manifests, source paths and conventional directory names may be insufficient; ambiguous or missing ownership remains a review question.
|
|
40
|
+
|
|
41
|
+
## Limits and output
|
|
42
|
+
|
|
43
|
+
- Scan at most **1,500 directory entries**, breadth-first. Root manifests and shallow sibling projects are examined before deep trees; conventional first-party directories (src, include, apps, packages, libs, services) are queued ahead of other siblings. Very wide directories can still exhaust the budget.
|
|
44
|
+
- Read at most **256 KiB per manifest** and accept at most **2 MiB of manifest content**. Oversized, malformed and budget-skipped manifests generate warnings. Source files are identified by path; their contents are left for agent verification.
|
|
45
|
+
- Skip symlinks, `.env*`, dependencies, common generated output (including `.vite` dependency caches), virtual environments and native build/dependency directories such as `third_party`, `third-party`, `thirdparty`, `external`, `extern`, `deps`, `dependencies`, `Pods`, `Carthage`, `DerivedData`, `.build`, `.gradle` and `.dart_tool`. This uses a built-in exclusion list, not a complete `.gitignore` implementation.
|
|
46
|
+
- Return at most **50 project candidates**, **30 file/directory ownership hints per chapter**, **6 evidence paths per detection**, **10 warning details**, and **30 unclassified/sample-omitted paths**. Total counts and truncation flags accompany capped sections. A file has at most one proposed owner; incomplete hints must be expanded or reassigned during review.
|
|
47
|
+
|
|
48
|
+
On a complete scan, homogeneous subdirectories containing only one candidate’s files may collapse to a directory hint. The repository root, mixed-language directories, directories with unclassified files, nested owners or skipped descendants, and truncated scans retain narrower hints. Directory ownership still requires source review: filenames cannot establish semantic independence. `directoryHints`, `matchedFileCount`, `ownershipCoverage` and `pathsTruncated` distinguish compact directory boundaries from incomplete samples.
|
|
49
|
+
|
|
50
|
+
`ownershipIncomplete` and `next` prominently flag scan, project or ownership-hint truncation. They do not imply completeness when false: unclassified, excluded and semantically mixed responsibilities still need review. Files covered by directory hints no longer appear as unclassified merely because the returned path list is short.
|
|
51
|
+
|
|
52
|
+
`coveragePrompts` lists detected delivery-related filenames and asks about versioning, release triggers, build/test artifacts, publication gates and external distribution steps. It never infers release behavior from a filename or creates a fact. A passing registry check remains a mechanical consistency check, not a completeness guarantee.
|
|
53
|
+
|
|
54
|
+
A separate format-architecture prompt requires three distinct implementation files whose names suggest header validation, node/chunk writing and hierarchy serialization, plus a recognizable format specification document. Directory names alone do not qualify. Up to ten evidence paths retain a representative for each role and the specification. This is a conservative review prompt: the agent must verify the relationships in source, establish usefulness beyond one query, and check existing ownership before proposing an entry. The prompt never creates a navigation entry, fact, chapter or pillar; publication still needs developer direction.
|
|
55
|
+
|
|
56
|
+
`scan.truncated` reports filesystem scan truncation. `scan.inspectedEntries`, `scan.inspectedFiles` and `scan.manifestBytesRead` describe the work performed. `projectsTruncated`, `detectedProjectCount`, each detection's `pathsTruncated`/`evidenceCount`, and `warningCount` describe output limits. Inspect these fields before claiming the repository has been covered. A successful scan never proves exhaustive coverage; these discovery limits do not cap stored facts or approved projects.
|
|
57
|
+
|
|
58
|
+
Invalid JSON is reported without aborting the entire bootstrap. Filename/language signals can still yield a coarse candidate. Dynamic JS/TS configuration, imported dependency catalogs, custom project layouts, linked external roots and unsupported ecosystems need source review or additional built-in detection rules. New framework support currently means editing `src/discovery.ts` and adding synthetic regression fixtures; there is no plugin/provider API yet.
|
|
59
|
+
|
|
60
|
+
## Extending safely
|
|
61
|
+
|
|
62
|
+
Repository owners can define an approved pillar/chapter for an unsupported technology today. Automatic discovery is a convenience, not a prerequisite for using the framework. For new built-in discovery rules, preserve bounded reads, explicit uncertainty, approval requirements and non-overlapping ownership. Add positive examples alongside false-positive, mixed-monorepo and malformed-input cases. Never populate factual claims from detection labels alone.
|
|
63
|
+
|
|
64
|
+
C/C++ source extensions and CMake/Meson manifests produce C/C++ responsibility candidates. Root C/C++ signals take precedence over incidental PHP utilities in the same project root; nested manifest roots remain separate candidates. These are heuristics and require boundary review.
|
|
65
|
+
|
|
66
|
+
### Explicit scan boundaries
|
|
67
|
+
|
|
68
|
+
Use `cground scan --exclude odd-bundle,tools/generated` (MCP scan `exclude: ["odd-bundle", "tools/generated"]`) for repository-specific literal paths. Exclusions apply before descent, accept trailing slashes, and reject absolute or traversal paths. They apply only to this scan; inspect and use its returned map for bootstrap. They do not change existing chapter ownership or source verification.
|
|
69
|
+
|
|
70
|
+
Built-in exclusions also cover `dep`, `bundled`, `vendored`, `.vscode`, `.idea`, `.codex`, `.claude` and `.cursor`. Directory-name matching is case-insensitive. `lib` and `libs` remain eligible because they often contain first-party code. Unknown dependency layouts still require explicit exclusions and source review; discovery does not prove authorship. `scan.skipped` reports up to 30 exclusions with reasons and `skippedCount` reports the total. Environment filenames are redacted. Excluded subtrees consume one encountered entry but none of their descendants consume the traversal budget.
|