@stackbilt/cli 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +17 -1
  2. package/README.md +42 -0
  3. package/dist/__tests__/bootstrap.test.d.ts +2 -0
  4. package/dist/__tests__/bootstrap.test.d.ts.map +1 -0
  5. package/dist/__tests__/bootstrap.test.js +134 -0
  6. package/dist/__tests__/bootstrap.test.js.map +1 -0
  7. package/dist/__tests__/integration/precommit-hook.test.js +4 -4
  8. package/dist/__tests__/score.test.d.ts +2 -0
  9. package/dist/__tests__/score.test.d.ts.map +1 -0
  10. package/dist/__tests__/score.test.js +234 -0
  11. package/dist/__tests__/score.test.js.map +1 -0
  12. package/dist/commands/adf-tidy.d.ts.map +1 -1
  13. package/dist/commands/adf-tidy.js +6 -3
  14. package/dist/commands/adf-tidy.js.map +1 -1
  15. package/dist/commands/adf.js +20 -13
  16. package/dist/commands/adf.js.map +1 -1
  17. package/dist/commands/audit.js +24 -7
  18. package/dist/commands/audit.js.map +1 -1
  19. package/dist/commands/blast.d.ts +12 -0
  20. package/dist/commands/blast.d.ts.map +1 -0
  21. package/dist/commands/blast.js +208 -0
  22. package/dist/commands/blast.js.map +1 -0
  23. package/dist/commands/bootstrap.d.ts.map +1 -1
  24. package/dist/commands/bootstrap.js +245 -101
  25. package/dist/commands/bootstrap.js.map +1 -1
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +9 -2
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +127 -8
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/run.js +1 -1
  33. package/dist/commands/run.js.map +1 -1
  34. package/dist/commands/score.d.ts +9 -0
  35. package/dist/commands/score.d.ts.map +1 -0
  36. package/dist/commands/score.js +1273 -0
  37. package/dist/commands/score.js.map +1 -0
  38. package/dist/commands/setup.js +2 -2
  39. package/dist/commands/setup.js.map +1 -1
  40. package/dist/commands/surface.d.ts +15 -0
  41. package/dist/commands/surface.d.ts.map +1 -0
  42. package/dist/commands/surface.js +112 -0
  43. package/dist/commands/surface.js.map +1 -0
  44. package/dist/http-client.d.ts +13 -4
  45. package/dist/http-client.d.ts.map +1 -1
  46. package/dist/http-client.js +1 -1
  47. package/dist/http-client.js.map +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +22 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/types/scaffold-contract-types.d.ts +90 -0
  52. package/dist/types/scaffold-contract-types.d.ts.map +1 -0
  53. package/dist/types/scaffold-contract-types.js +22 -0
  54. package/dist/types/scaffold-contract-types.js.map +1 -0
  55. package/package.json +12 -9
@@ -0,0 +1,1273 @@
1
+ "use strict";
2
+ /**
3
+ * charter score
4
+ *
5
+ * Deterministic, local AI-readiness audit for any repository.
6
+ * Scores agent config, grounding, architecture, testing, governance, and freshness.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.scoreCommand = scoreCommand;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const adf_1 = require("@stackbilt/adf");
46
+ const index_1 = require("../index");
47
+ const flags_1 = require("../flags");
48
+ const setup_1 = require("./setup");
49
+ const git_helpers_1 = require("../git-helpers");
50
+ const adf_2 = require("./adf");
51
+ const AGENT_FILES = ['CLAUDE.md', 'AGENTS.md', 'agents.md', 'GEMINI.md', 'copilot-instructions.md'];
52
+ const ROOT_DOC_FILES = ['README.md', 'CONTRIBUTING.md'];
53
+ const KNOWN_COMMANDS = new Set([
54
+ 'bash',
55
+ 'bun',
56
+ 'cargo',
57
+ 'cd',
58
+ 'charter',
59
+ 'deno',
60
+ 'docker',
61
+ 'docker-compose',
62
+ 'git',
63
+ 'go',
64
+ 'just',
65
+ 'make',
66
+ 'node',
67
+ 'npm',
68
+ 'npx',
69
+ 'pip',
70
+ 'pnpm',
71
+ 'pnpx',
72
+ 'poetry',
73
+ 'pytest',
74
+ 'python',
75
+ 'sh',
76
+ 'tsx',
77
+ 'turbo',
78
+ 'uv',
79
+ 'vitest',
80
+ 'yarn',
81
+ ]);
82
+ const KNOWN_PATH_FILENAMES = new Set([
83
+ '.cursorrules',
84
+ 'AGENTS.md',
85
+ 'CLAUDE.md',
86
+ 'CHANGELOG.md',
87
+ 'CONTRIBUTING.md',
88
+ 'Cargo.toml',
89
+ 'Dockerfile',
90
+ 'Jenkinsfile',
91
+ 'Makefile',
92
+ 'README.md',
93
+ 'package.json',
94
+ 'pnpm-lock.yaml',
95
+ 'pnpm-workspace.yaml',
96
+ 'pyproject.toml',
97
+ 'tsconfig.json',
98
+ 'vitest.config.ts',
99
+ 'vitest.config.mts',
100
+ 'wrangler.toml',
101
+ ]);
102
+ const KNOWN_PATH_EXTENSIONS = new Set([
103
+ '.adf',
104
+ '.cjs',
105
+ '.conf',
106
+ '.config',
107
+ '.css',
108
+ '.go',
109
+ '.graphql',
110
+ '.h',
111
+ '.hpp',
112
+ '.html',
113
+ '.java',
114
+ '.js',
115
+ '.json',
116
+ '.jsx',
117
+ '.md',
118
+ '.mdc',
119
+ '.mdx',
120
+ '.mjs',
121
+ '.php',
122
+ '.py',
123
+ '.rb',
124
+ '.rs',
125
+ '.sh',
126
+ '.sql',
127
+ '.toml',
128
+ '.ts',
129
+ '.tsx',
130
+ '.txt',
131
+ '.yaml',
132
+ '.yml',
133
+ ]);
134
+ const CODE_EXTENSIONS = new Set([
135
+ '.c',
136
+ '.cc',
137
+ '.cpp',
138
+ '.cs',
139
+ '.go',
140
+ '.java',
141
+ '.js',
142
+ '.jsx',
143
+ '.kt',
144
+ '.mjs',
145
+ '.mts',
146
+ '.php',
147
+ '.py',
148
+ '.rb',
149
+ '.rs',
150
+ '.scala',
151
+ '.sh',
152
+ '.sql',
153
+ '.swift',
154
+ '.ts',
155
+ '.tsx',
156
+ ]);
157
+ const CI_FILES = [
158
+ '.github/workflows',
159
+ '.gitlab-ci.yml',
160
+ '.circleci/config.yml',
161
+ 'azure-pipelines.yml',
162
+ 'buildkite.yml',
163
+ 'Jenkinsfile',
164
+ ];
165
+ const STATE_DOC_FILES = ['STATE.md', 'STATUS.md', 'ROADMAP.md', 'TODO.md', 'CHANGELOG.md'];
166
+ const WALK_IGNORE_DIRS = new Set([
167
+ '.git',
168
+ '.next',
169
+ '.turbo',
170
+ '.yarn',
171
+ 'coverage',
172
+ 'dist',
173
+ 'build',
174
+ 'node_modules',
175
+ 'target',
176
+ ]);
177
+ const MAX_TEXT_FILE_BYTES = 256 * 1024;
178
+ const GIT_TIMESTAMP_FILE_LIMIT = 4000;
179
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
180
+ const CI_MIN_SCORE = 60;
181
+ async function scoreCommand(options, args = []) {
182
+ if (args.includes('--help') || args.includes('-h')) {
183
+ printHelp();
184
+ return index_1.EXIT_CODE.SUCCESS;
185
+ }
186
+ const aiDir = normalizeRelativePath((0, flags_1.getFlag)(args, '--ai-dir') || '.ai');
187
+ const inventory = collectRepoInventory();
188
+ const report = buildScoreReport(inventory, aiDir);
189
+ if (options.format === 'json') {
190
+ console.log(JSON.stringify(report, null, 2));
191
+ }
192
+ else {
193
+ printScoreReport(report);
194
+ }
195
+ if (options.ciMode && report.score.total < CI_MIN_SCORE) {
196
+ return index_1.EXIT_CODE.POLICY_VIOLATION;
197
+ }
198
+ return index_1.EXIT_CODE.SUCCESS;
199
+ }
200
+ function buildScoreReport(inventory, aiDir) {
201
+ const contexts = (0, setup_1.loadPackageContexts)();
202
+ const packageManager = (0, setup_1.detectPackageManager)(contexts);
203
+ const repo = (0, setup_1.inferProjectName)(contexts);
204
+ const textCache = new Map();
205
+ const readText = (relativePath) => {
206
+ if (textCache.has(relativePath)) {
207
+ return textCache.get(relativePath) || '';
208
+ }
209
+ const absolutePath = path.resolve(relativePath);
210
+ try {
211
+ const stat = fs.statSync(absolutePath);
212
+ if (!stat.isFile() || stat.size > MAX_TEXT_FILE_BYTES) {
213
+ textCache.set(relativePath, null);
214
+ return '';
215
+ }
216
+ const content = fs.readFileSync(absolutePath, 'utf-8');
217
+ textCache.set(relativePath, content);
218
+ return content;
219
+ }
220
+ catch {
221
+ textCache.set(relativePath, null);
222
+ return '';
223
+ }
224
+ };
225
+ const manifestPath = normalizeRelativePath(path.posix.join(aiDir, 'manifest.adf'));
226
+ const aiFiles = inventory.files.filter((file) => file.startsWith(`${aiDir}/`) && file.endsWith('.adf'));
227
+ const alternateAgentFiles = AGENT_FILES.filter((file) => inventory.fileSet.has(file) && file !== 'CLAUDE.md' && !/^agents\.md$/i.test(file));
228
+ const claudeContent = readText('CLAUDE.md');
229
+ const claudeExists = inventory.fileSet.has('CLAUDE.md');
230
+ const claudeSubstantive = claudeExists && isSubstantiveInstruction(claudeContent);
231
+ const cursorFiles = inventory.fileSet.has('.cursorrules')
232
+ ? ['.cursorrules']
233
+ : inventory.files.filter((file) => file.startsWith('.cursor/rules/'));
234
+ const cursorContent = cursorFiles.map((file) => readText(file)).join('\n');
235
+ const cursorExists = cursorFiles.length > 0;
236
+ const cursorSubstantive = cursorExists && isSubstantiveInstruction(cursorContent);
237
+ const agentsFile = inventory.fileSet.has('AGENTS.md')
238
+ ? 'AGENTS.md'
239
+ : inventory.fileSet.has('agents.md')
240
+ ? 'agents.md'
241
+ : undefined;
242
+ const agentsContent = agentsFile ? readText(agentsFile) : '';
243
+ const agentsExists = !!agentsFile;
244
+ const agentsSubstantive = agentsExists && isSubstantiveInstruction(agentsContent);
245
+ const manifestExists = inventory.fileSet.has(manifestPath);
246
+ let agentConfigScore = 0;
247
+ if (claudeExists)
248
+ agentConfigScore += 5;
249
+ if (claudeSubstantive)
250
+ agentConfigScore += 5;
251
+ if (cursorExists)
252
+ agentConfigScore += cursorSubstantive ? 5 : 2;
253
+ if (agentsExists)
254
+ agentConfigScore += agentsSubstantive ? 5 : 2;
255
+ if (manifestExists)
256
+ agentConfigScore += 5;
257
+ const groundingFiles = [...new Set(AGENT_FILES.filter((file) => inventory.fileSet.has(file))
258
+ .concat(cursorFiles)
259
+ .concat(ROOT_DOC_FILES.filter((file) => inventory.fileSet.has(file)))
260
+ .concat(manifestExists ? [manifestPath] : [])
261
+ .concat(aiFiles.filter((file) => file !== manifestPath)))];
262
+ const pathReferences = new Map();
263
+ const commandSignals = [];
264
+ for (const file of groundingFiles) {
265
+ const content = readText(file);
266
+ if (!content)
267
+ continue;
268
+ for (const candidate of extractPathCandidates(content)) {
269
+ const reference = resolveReferencedPath(file, candidate);
270
+ pathReferences.set(`${reference.source}:${reference.resolved}`, reference);
271
+ }
272
+ commandSignals.push(...extractCommandSignals(file, content, inventory.fileSet, packageManager));
273
+ }
274
+ const brokenPaths = [...pathReferences.values()]
275
+ .filter((reference) => !pathExists(reference.resolved))
276
+ .map((reference) => reference.resolved);
277
+ const validPaths = pathReferences.size - brokenPaths.length;
278
+ const documentedTestCommands = [...new Set(commandSignals
279
+ .filter((signal) => signal.runnable && signal.kind === 'test')
280
+ .map((signal) => signal.command))];
281
+ const invalidCommands = commandSignals
282
+ .filter((signal) => !signal.runnable)
283
+ .map((signal) => `${signal.file}: ${signal.command}`);
284
+ const runnableCommandCount = commandSignals.filter((signal) => signal.runnable).length;
285
+ const pathScore = pathReferences.size > 0
286
+ ? Math.round(10 * (validPaths / pathReferences.size))
287
+ : 0;
288
+ const commandScore = commandSignals.length > 0
289
+ ? Math.round(10 * (((runnableCommandCount / commandSignals.length) + Math.min(1, runnableCommandCount / 2)) / 2))
290
+ : 0;
291
+ const groundingScore = pathScore + commandScore;
292
+ let manifestParsed = false;
293
+ let manifestDoc = null;
294
+ let manifestParseError;
295
+ let manifestDefaultLoad = [];
296
+ let manifestOnDemand = [];
297
+ let referencedModules = [];
298
+ const existingModules = [];
299
+ const missingModules = [];
300
+ const constraintSources = new Set();
301
+ let stateDefined = false;
302
+ let stateSource;
303
+ if (manifestExists) {
304
+ try {
305
+ manifestDoc = (0, adf_1.parseAdf)(readText(manifestPath));
306
+ const manifest = (0, adf_1.parseManifest)(manifestDoc);
307
+ manifestParsed = true;
308
+ manifestDefaultLoad = manifest.defaultLoad;
309
+ manifestOnDemand = manifest.onDemand.map((mod) => mod.path);
310
+ referencedModules = [...new Set([...manifest.defaultLoad, ...manifest.onDemand.map((mod) => mod.path)])];
311
+ for (const modulePath of referencedModules) {
312
+ const normalizedModule = normalizeRelativePath(path.posix.join(aiDir, modulePath));
313
+ if (!inventory.fileSet.has(normalizedModule)) {
314
+ missingModules.push(normalizedModule);
315
+ continue;
316
+ }
317
+ existingModules.push(normalizedModule);
318
+ try {
319
+ const moduleDoc = (0, adf_1.parseAdf)(readText(normalizedModule));
320
+ if (hasConstraints(moduleDoc)) {
321
+ constraintSources.add(normalizedModule);
322
+ }
323
+ if (!stateDefined && hasAdfState(moduleDoc)) {
324
+ stateDefined = true;
325
+ stateSource = normalizedModule;
326
+ }
327
+ }
328
+ catch {
329
+ // Keep module presence signal even if the content is malformed.
330
+ }
331
+ }
332
+ }
333
+ catch (error) {
334
+ manifestParseError = error instanceof Error ? error.message : String(error);
335
+ }
336
+ }
337
+ if (constraintSources.size === 0) {
338
+ for (const file of groundingFiles) {
339
+ if (hasDocumentedConstraints(readText(file))) {
340
+ constraintSources.add(file);
341
+ }
342
+ }
343
+ }
344
+ if (!stateDefined) {
345
+ const fallbackStateFile = STATE_DOC_FILES.find((file) => inventory.fileSet.has(file));
346
+ if (fallbackStateFile) {
347
+ stateDefined = true;
348
+ stateSource = fallbackStateFile;
349
+ }
350
+ }
351
+ const manifestScore = manifestParsed ? 8 : manifestExists ? 4 : 0;
352
+ const moduleScore = referencedModules.length > 0
353
+ ? Math.round(5 * (existingModules.length / referencedModules.length))
354
+ : aiFiles.length > 0
355
+ ? 2
356
+ : 0;
357
+ const constraintsScore = constraintSources.size > 0 ? 4 : 0;
358
+ const stateScore = stateDefined ? 3 : 0;
359
+ const architectureScore = manifestScore + moduleScore + constraintsScore + stateScore;
360
+ const derivedTestCommands = deriveTestCommands(inventory, packageManager);
361
+ const documentedTestingScore = documentedTestCommands.length >= 2
362
+ ? 8
363
+ : documentedTestCommands.length === 1
364
+ ? 6
365
+ : derivedTestCommands.length > 0
366
+ ? 5
367
+ : 0;
368
+ const ciFiles = detectCiFiles(inventory);
369
+ const ciScore = ciFiles.length > 0 ? 7 : 0;
370
+ const testingScore = documentedTestingScore + ciScore;
371
+ const skillFiles = inventory.files.filter((file) => path.posix.basename(file) === 'SKILL.md');
372
+ const permissionFiles = detectPermissionFiles(inventory, readText);
373
+ const hookFiles = detectHookFiles(inventory);
374
+ const governanceScore = (skillFiles.length > 0 ? 4 : 0)
375
+ + (permissionFiles.length > 0 ? 3 : 0)
376
+ + (hookFiles.length > 0 ? 3 : 0);
377
+ const configFiles = [...new Set([
378
+ ...AGENT_FILES.filter((file) => inventory.fileSet.has(file)),
379
+ ...cursorFiles,
380
+ manifestPath,
381
+ ...aiFiles,
382
+ ...ciFiles,
383
+ ...skillFiles,
384
+ ...permissionFiles,
385
+ ...hookFiles.filter((file) => !file.startsWith('.git/')),
386
+ '.charter/config.json',
387
+ ].filter((file) => file && fileExists(file)))];
388
+ const codeFiles = detectCodeFiles(inventory, configFiles);
389
+ const freshnessSignals = evaluateFreshness(configFiles, codeFiles);
390
+ const freshnessScore = scoreFreshness(freshnessSignals);
391
+ const categories = [
392
+ createCategory('agent-config', 'Agent config', agentConfigScore, 25, agentConfigSummary(claudeExists, claudeSubstantive, cursorFiles, cursorSubstantive, agentsFile, agentsSubstantive, manifestExists, manifestPath)),
393
+ createCategory('grounding', 'Grounding', groundingScore, 20, groundingSummary(pathReferences.size, validPaths, brokenPaths.length, commandSignals.length, runnableCommandCount)),
394
+ createCategory('architecture', 'Architecture', architectureScore, 20, architectureSummary(manifestExists, manifestParsed, existingModules.length, missingModules.length, constraintSources.size > 0, stateDefined)),
395
+ createCategory('testing', 'Testing', testingScore, 15, testingSummary(documentedTestCommands, derivedTestCommands, ciFiles)),
396
+ createCategory('governance', 'Governance', governanceScore, 10, governanceSummary(skillFiles, permissionFiles, hookFiles)),
397
+ createCategory('freshness', 'Freshness', freshnessScore, 10, freshnessSummary(freshnessSignals)),
398
+ ];
399
+ const total = categories.reduce((sum, category) => sum + category.score, 0);
400
+ const report = {
401
+ repo,
402
+ generatedAt: new Date().toISOString(),
403
+ score: {
404
+ total,
405
+ grade: toGrade(total),
406
+ },
407
+ categories,
408
+ recommendations: buildRecommendations({
409
+ aiDir,
410
+ manifestPath,
411
+ claudeExists,
412
+ claudeSubstantive,
413
+ cursorFiles,
414
+ agentsFile,
415
+ manifestExists,
416
+ brokenPaths,
417
+ commandSignals,
418
+ documentedTestCommands,
419
+ derivedTestCommands,
420
+ manifestParsed,
421
+ missingModules,
422
+ constraintsDefined: constraintSources.size > 0,
423
+ stateDefined,
424
+ ciFiles,
425
+ skillFiles,
426
+ permissionFiles,
427
+ hookFiles,
428
+ freshnessSignals,
429
+ }),
430
+ signals: {
431
+ agentConfig: {
432
+ claude: { exists: claudeExists, substantive: claudeSubstantive },
433
+ cursorRules: { exists: cursorExists, substantive: cursorSubstantive, files: cursorFiles },
434
+ agents: { exists: agentsExists, substantive: agentsSubstantive, file: agentsFile },
435
+ manifest: { exists: manifestExists, path: manifestPath },
436
+ alternateAgentFiles,
437
+ },
438
+ grounding: {
439
+ filesScanned: groundingFiles,
440
+ pathReferences: {
441
+ total: pathReferences.size,
442
+ valid: validPaths,
443
+ broken: brokenPaths,
444
+ },
445
+ commands: {
446
+ total: commandSignals.length,
447
+ runnable: runnableCommandCount,
448
+ invalid: invalidCommands,
449
+ documentedTestCommands,
450
+ },
451
+ },
452
+ architecture: {
453
+ manifest: {
454
+ exists: manifestExists,
455
+ parsed: manifestParsed,
456
+ defaultLoad: manifestDefaultLoad,
457
+ onDemand: manifestOnDemand,
458
+ parseError: manifestParseError,
459
+ },
460
+ modules: {
461
+ total: referencedModules.length,
462
+ existing: existingModules,
463
+ missing: missingModules,
464
+ },
465
+ constraints: {
466
+ defined: constraintSources.size > 0,
467
+ sources: [...constraintSources],
468
+ },
469
+ state: {
470
+ defined: stateDefined,
471
+ source: stateSource,
472
+ },
473
+ },
474
+ testing: {
475
+ packageManager,
476
+ documentedCommands: documentedTestCommands,
477
+ derivedCommands: derivedTestCommands,
478
+ ciFiles,
479
+ },
480
+ governance: {
481
+ skillFiles,
482
+ permissionFiles,
483
+ hookFiles,
484
+ },
485
+ freshness: freshnessSignals,
486
+ },
487
+ };
488
+ return report;
489
+ }
490
+ function createCategory(id, label, score, max, summary) {
491
+ return {
492
+ id,
493
+ label,
494
+ score,
495
+ max,
496
+ status: toCategoryStatus(score, max),
497
+ summary,
498
+ };
499
+ }
500
+ function collectRepoInventory() {
501
+ const files = collectRepoFiles();
502
+ return {
503
+ files,
504
+ fileSet: new Set(files),
505
+ };
506
+ }
507
+ function collectRepoFiles() {
508
+ if ((0, git_helpers_1.isGitRepo)()) {
509
+ try {
510
+ const output = (0, git_helpers_1.runGit)(['ls-files', '--cached', '--others', '--exclude-standard']);
511
+ const files = output
512
+ .split(/\r?\n/)
513
+ .map((line) => normalizeRelativePath(line))
514
+ .filter(Boolean);
515
+ if (files.length > 0) {
516
+ return [...new Set(files)];
517
+ }
518
+ }
519
+ catch {
520
+ // Fall through to filesystem walk.
521
+ }
522
+ }
523
+ const files = [];
524
+ walkRepo(process.cwd(), '', files);
525
+ return [...new Set(files)];
526
+ }
527
+ function walkRepo(absoluteDir, relativeDir, files) {
528
+ let entries = [];
529
+ try {
530
+ entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
531
+ }
532
+ catch {
533
+ return;
534
+ }
535
+ for (const entry of entries) {
536
+ if (entry.name === '.' || entry.name === '..')
537
+ continue;
538
+ const nextRelative = normalizeRelativePath(relativeDir ? `${relativeDir}/${entry.name}` : entry.name);
539
+ const nextAbsolute = path.join(absoluteDir, entry.name);
540
+ if (entry.isDirectory()) {
541
+ if (WALK_IGNORE_DIRS.has(entry.name))
542
+ continue;
543
+ walkRepo(nextAbsolute, nextRelative, files);
544
+ continue;
545
+ }
546
+ if (!entry.isFile())
547
+ continue;
548
+ files.push(nextRelative);
549
+ }
550
+ }
551
+ function deriveTestCommands(inventory, packageManager) {
552
+ const commands = new Set();
553
+ for (const packageJsonPath of inventory.files.filter((file) => path.posix.basename(file) === 'package.json')) {
554
+ try {
555
+ const raw = fs.readFileSync(path.resolve(packageJsonPath), 'utf-8');
556
+ const parsed = JSON.parse(raw);
557
+ const scripts = parsed.scripts || {};
558
+ for (const name of Object.keys(scripts)) {
559
+ if (name === 'test') {
560
+ commands.add(`${packageManager} test`);
561
+ }
562
+ else if (name.startsWith('test:')) {
563
+ commands.add(`${packageManager} run ${name}`);
564
+ }
565
+ }
566
+ }
567
+ catch {
568
+ // ignore malformed package files
569
+ }
570
+ }
571
+ if (inventory.fileSet.has('pytest.ini') || inventory.fileSet.has('pyproject.toml') || inventory.fileSet.has('tox.ini')) {
572
+ commands.add('pytest');
573
+ }
574
+ if (inventory.fileSet.has('Cargo.toml')) {
575
+ commands.add('cargo test');
576
+ }
577
+ if (inventory.fileSet.has('go.mod') || inventory.files.some((file) => file.endsWith('_test.go'))) {
578
+ commands.add('go test ./...');
579
+ }
580
+ if (inventory.files.some((file) => /^vitest\.config\.(ts|mts|js|mjs)$/i.test(path.posix.basename(file)))) {
581
+ commands.add(`${packageManager} exec vitest run`);
582
+ }
583
+ if (inventory.files.some((file) => /^jest\.config\.(ts|js|cjs|mjs)$/i.test(path.posix.basename(file)))) {
584
+ commands.add(`${packageManager} exec jest`);
585
+ }
586
+ return [...commands];
587
+ }
588
+ function detectCiFiles(inventory) {
589
+ const results = new Set();
590
+ for (const prefix of CI_FILES) {
591
+ if (prefix === '.github/workflows') {
592
+ for (const file of inventory.files.filter((candidate) => candidate.startsWith('.github/workflows/'))) {
593
+ results.add(file);
594
+ }
595
+ continue;
596
+ }
597
+ if (inventory.fileSet.has(prefix)) {
598
+ results.add(prefix);
599
+ }
600
+ }
601
+ return [...results];
602
+ }
603
+ function detectPermissionFiles(inventory, readText) {
604
+ const candidates = inventory.files.filter((file) => (file === 'CLAUDE.md'
605
+ || file === 'AGENTS.md'
606
+ || file === 'agents.md'
607
+ || file === '.cursorrules'
608
+ || file.startsWith('.claude/')
609
+ || file.startsWith('.codex/')
610
+ || file.startsWith('.cursor/')));
611
+ const matches = [];
612
+ for (const file of candidates) {
613
+ const content = readText(file);
614
+ if (!content)
615
+ continue;
616
+ if (/\b(permissions?|sandbox|approval|allowlist|denylist)\b/i.test(content) || /["'](allow|deny)["']\s*:/i.test(content)) {
617
+ matches.push(file);
618
+ }
619
+ }
620
+ return [...new Set(matches)];
621
+ }
622
+ function detectHookFiles(inventory) {
623
+ const hooks = new Set();
624
+ for (const file of ['.husky/pre-commit', '.husky/commit-msg', '.githooks/pre-commit', '.githooks/commit-msg']) {
625
+ if (inventory.fileSet.has(file)) {
626
+ hooks.add(file);
627
+ }
628
+ }
629
+ for (const file of ['.git/hooks/pre-commit', '.git/hooks/commit-msg']) {
630
+ if (fs.existsSync(path.resolve(file))) {
631
+ hooks.add(file);
632
+ }
633
+ }
634
+ if ((0, git_helpers_1.isGitRepo)()) {
635
+ try {
636
+ const configuredHooksPath = (0, git_helpers_1.runGit)(['config', '--get', 'core.hooksPath']).trim();
637
+ if (configuredHooksPath) {
638
+ for (const file of ['pre-commit', 'commit-msg']) {
639
+ const relative = normalizeRelativePath(path.posix.join(configuredHooksPath.replace(/\\/g, '/'), file));
640
+ if (fs.existsSync(path.resolve(relative))) {
641
+ hooks.add(relative);
642
+ }
643
+ }
644
+ }
645
+ }
646
+ catch {
647
+ // no configured hooks path
648
+ }
649
+ }
650
+ return [...hooks];
651
+ }
652
+ function detectCodeFiles(inventory, configFiles) {
653
+ const configSet = new Set(configFiles.map((file) => normalizeRelativePath(file)));
654
+ const codeFiles = inventory.files.filter((file) => {
655
+ if (configSet.has(file))
656
+ return false;
657
+ if (file.endsWith('.md') || file.endsWith('.mdx') || file.endsWith('.adf'))
658
+ return false;
659
+ const base = path.posix.basename(file);
660
+ if (KNOWN_PATH_FILENAMES.has(base))
661
+ return false;
662
+ return CODE_EXTENSIONS.has(path.posix.extname(file).toLowerCase());
663
+ });
664
+ if (codeFiles.length > 0) {
665
+ return codeFiles;
666
+ }
667
+ return inventory.files.filter((file) => !configSet.has(file) && !file.startsWith('.git/'));
668
+ }
669
+ function evaluateFreshness(configFiles, codeFiles) {
670
+ const useGit = shouldUseGitTimestamps(configFiles, codeFiles);
671
+ const latestCode = getLatestTimestamp(codeFiles, useGit);
672
+ const latestConfig = getLatestTimestamp(configFiles, useGit);
673
+ if (!latestCode || !latestConfig) {
674
+ return {
675
+ strategy: latestCode || latestConfig ? (useGit ? 'git' : 'mtime') : 'none',
676
+ latestCodeChange: latestCode ? formatDate(latestCode) : undefined,
677
+ latestConfigChange: latestConfig ? formatDate(latestConfig) : undefined,
678
+ };
679
+ }
680
+ const deltaDays = Math.max(0, Math.round((latestCode.getTime() - latestConfig.getTime()) / MS_PER_DAY));
681
+ return {
682
+ strategy: useGit ? 'git' : 'mtime',
683
+ latestCodeChange: formatDate(latestCode),
684
+ latestConfigChange: formatDate(latestConfig),
685
+ deltaDays,
686
+ };
687
+ }
688
+ function scoreFreshness(freshness) {
689
+ if (!freshness.latestCodeChange || !freshness.latestConfigChange) {
690
+ return 0;
691
+ }
692
+ const deltaDays = freshness.deltaDays ?? 0;
693
+ if (deltaDays <= 7)
694
+ return 10;
695
+ if (deltaDays <= 30)
696
+ return 8;
697
+ if (deltaDays <= 90)
698
+ return 5;
699
+ if (deltaDays <= 180)
700
+ return 2;
701
+ return 0;
702
+ }
703
+ function shouldUseGitTimestamps(configFiles, codeFiles) {
704
+ return (0, git_helpers_1.isGitRepo)() && (0, git_helpers_1.hasCommits)() && configFiles.length > 0 && codeFiles.length > 0 && (configFiles.length + codeFiles.length) <= GIT_TIMESTAMP_FILE_LIMIT;
705
+ }
706
+ function getLatestTimestamp(files, useGit) {
707
+ const uniqueFiles = [...new Set(files.map((file) => normalizeRelativePath(file)).filter((file) => fileExists(file)))];
708
+ if (uniqueFiles.length === 0)
709
+ return null;
710
+ if (useGit) {
711
+ const fromGit = getLatestGitTimestamp(uniqueFiles);
712
+ if (fromGit) {
713
+ return fromGit;
714
+ }
715
+ }
716
+ let latest = 0;
717
+ for (const file of uniqueFiles) {
718
+ try {
719
+ const stat = fs.statSync(path.resolve(file));
720
+ if (stat.mtimeMs > latest) {
721
+ latest = stat.mtimeMs;
722
+ }
723
+ }
724
+ catch {
725
+ // ignore unreadable files
726
+ }
727
+ }
728
+ return latest > 0 ? new Date(latest) : null;
729
+ }
730
+ function getLatestGitTimestamp(files) {
731
+ if (!(0, git_helpers_1.isGitRepo)() || !(0, git_helpers_1.hasCommits)() || files.length === 0 || files.length > GIT_TIMESTAMP_FILE_LIMIT) {
732
+ return null;
733
+ }
734
+ let latestSeconds = 0;
735
+ for (let i = 0; i < files.length; i += 200) {
736
+ const chunk = files.slice(i, i + 200);
737
+ try {
738
+ const raw = (0, git_helpers_1.runGit)(['log', '-1', '--format=%ct', '--', ...chunk]).trim();
739
+ const seconds = Number.parseInt(raw, 10);
740
+ if (Number.isFinite(seconds) && seconds > latestSeconds) {
741
+ latestSeconds = seconds;
742
+ }
743
+ }
744
+ catch {
745
+ // Ignore chunk-level git failures; fallback handled by caller.
746
+ }
747
+ }
748
+ return latestSeconds > 0 ? new Date(latestSeconds * 1000) : null;
749
+ }
750
+ function hasConstraints(doc) {
751
+ return doc.sections.some((section) => (section.key === 'CONSTRAINTS'
752
+ || section.weight === 'load-bearing') && sectionHasContent(section.content));
753
+ }
754
+ function hasAdfState(doc) {
755
+ return doc.sections.some((section) => {
756
+ if (section.key !== 'STATE')
757
+ return false;
758
+ if (section.content.type === 'map') {
759
+ return section.content.entries.some((entry) => entry.key === 'CURRENT' || entry.key === 'NEXT');
760
+ }
761
+ return sectionHasContent(section.content);
762
+ });
763
+ }
764
+ function sectionHasContent(content) {
765
+ switch (content.type) {
766
+ case 'list':
767
+ return content.items.length > 0;
768
+ case 'map':
769
+ return content.entries.length > 0;
770
+ case 'metric':
771
+ return content.entries.length > 0;
772
+ case 'text':
773
+ return content.value.trim().length > 0;
774
+ default:
775
+ return false;
776
+ }
777
+ }
778
+ function hasDocumentedConstraints(content) {
779
+ if (!content)
780
+ return false;
781
+ return /^#{1,3}\s+(constraints|guardrails|rules|non-negotiables)\b/im.test(content)
782
+ || /\b(non-negotiable|must always|never do|guardrails?)\b/i.test(content);
783
+ }
784
+ function isSubstantiveInstruction(content) {
785
+ if (!content)
786
+ return false;
787
+ let normalized = content;
788
+ for (const marker of adf_2.POINTER_MARKERS) {
789
+ normalized = normalized.replaceAll(marker, '');
790
+ }
791
+ normalized = normalized
792
+ .replace(/auto-managed by Charter/gi, '')
793
+ .replace(/\.ai\/manifest\.adf/gi, '')
794
+ .replace(/Do not duplicate rules from \.ai\//gi, '')
795
+ .trim();
796
+ const nonEmptyLines = normalized
797
+ .split(/\r?\n/)
798
+ .map((line) => line.trim())
799
+ .filter(Boolean);
800
+ return nonEmptyLines.length >= 3 || normalized.length >= 80;
801
+ }
802
+ function extractPathCandidates(content) {
803
+ const candidates = new Set();
804
+ for (const match of content.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
805
+ const target = normalizePathCandidate(match[1]);
806
+ if (looksLikePath(target))
807
+ candidates.add(target);
808
+ }
809
+ for (const match of content.matchAll(/`([^`\n]+)`/g)) {
810
+ const candidate = normalizePathCandidate(match[1]);
811
+ if (looksLikePath(candidate))
812
+ candidates.add(candidate);
813
+ }
814
+ for (const match of content.matchAll(/(^|[\s(])((?:\.{1,2}\/)?(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+(?:\.[A-Za-z0-9._-]+)?)/gm)) {
815
+ const candidate = normalizePathCandidate(match[2]);
816
+ if (looksLikePath(candidate))
817
+ candidates.add(candidate);
818
+ }
819
+ return [...candidates];
820
+ }
821
+ function normalizePathCandidate(raw) {
822
+ return raw
823
+ .trim()
824
+ .replace(/^['"`(]+/, '')
825
+ .replace(/[),.:;"'`]+$/, '')
826
+ .replace(/#.*/, '')
827
+ .replace(/^\.\//, '')
828
+ .replace(/\\/g, '/');
829
+ }
830
+ function resolveReferencedPath(sourceFile, candidate) {
831
+ if (candidate.startsWith('/')) {
832
+ return {
833
+ source: sourceFile,
834
+ candidate,
835
+ resolved: normalizeRelativePath(candidate),
836
+ };
837
+ }
838
+ const sourceDir = path.posix.dirname(sourceFile);
839
+ const resolved = sourceDir === '.'
840
+ ? normalizeRelativePath(candidate)
841
+ : normalizeRelativePath(path.posix.join(sourceDir, candidate));
842
+ return {
843
+ source: sourceFile,
844
+ candidate,
845
+ resolved,
846
+ };
847
+ }
848
+ function looksLikePath(candidate) {
849
+ if (!candidate)
850
+ return false;
851
+ if (candidate.startsWith('http://') || candidate.startsWith('https://') || candidate.startsWith('mailto:'))
852
+ return false;
853
+ if (candidate.startsWith('#') || candidate.startsWith('--'))
854
+ return false;
855
+ if (candidate.includes('<') || candidate.includes('>') || candidate.includes('*') || candidate.includes('${'))
856
+ return false;
857
+ if (candidate.includes(' '))
858
+ return false;
859
+ if (KNOWN_PATH_FILENAMES.has(candidate) || KNOWN_PATH_FILENAMES.has(path.posix.basename(candidate)))
860
+ return true;
861
+ if (candidate.includes('/'))
862
+ return true;
863
+ return KNOWN_PATH_EXTENSIONS.has(path.posix.extname(candidate).toLowerCase());
864
+ }
865
+ function pathExists(relativePath) {
866
+ return fileExists(relativePath);
867
+ }
868
+ function fileExists(relativePath) {
869
+ try {
870
+ return fs.existsSync(path.resolve(relativePath));
871
+ }
872
+ catch {
873
+ return false;
874
+ }
875
+ }
876
+ function extractCommandSignals(file, content, fileSet, packageManager) {
877
+ const signals = [];
878
+ for (const block of extractFencedCodeBlocks(content)) {
879
+ if (!isShellLikeBlock(block.language))
880
+ continue;
881
+ const lines = block.content.split(/\r?\n/);
882
+ let captured = 0;
883
+ for (const rawLine of lines) {
884
+ const cleaned = normalizeCommandLine(rawLine);
885
+ if (!cleaned)
886
+ continue;
887
+ captured++;
888
+ signals.push({
889
+ file,
890
+ command: cleaned,
891
+ runnable: isRunnableCommand(cleaned, fileSet, packageManager),
892
+ kind: isTestCommand(cleaned) ? 'test' : 'general',
893
+ });
894
+ if (captured >= 8)
895
+ break;
896
+ }
897
+ }
898
+ return signals;
899
+ }
900
+ function extractFencedCodeBlocks(content) {
901
+ const blocks = [];
902
+ for (const match of content.matchAll(/```([^\n`]*)\n([\s\S]*?)```/g)) {
903
+ blocks.push({
904
+ language: match[1].trim().toLowerCase(),
905
+ content: match[2],
906
+ });
907
+ }
908
+ return blocks;
909
+ }
910
+ function isShellLikeBlock(language) {
911
+ return language === ''
912
+ || language === 'bash'
913
+ || language === 'console'
914
+ || language === 'shell'
915
+ || language === 'sh'
916
+ || language === 'zsh';
917
+ }
918
+ function normalizeCommandLine(rawLine) {
919
+ let line = rawLine.trim();
920
+ if (!line || line.startsWith('#'))
921
+ return '';
922
+ line = line.replace(/^(?:\$|>|%|\u203A)\s+/, '').trim();
923
+ if (!line || line.startsWith('#'))
924
+ return '';
925
+ if (/\b(todo|placeholder)\b/i.test(line) || line.includes('<') || /(^|\s)\.\.\.(\s|$)/.test(line))
926
+ return '';
927
+ return line;
928
+ }
929
+ function isRunnableCommand(command, fileSet, packageManager) {
930
+ const segment = command.split(/\s*(?:&&|\|\||;|\|)\s*/)[0].trim();
931
+ if (!segment)
932
+ return false;
933
+ const parts = segment.split(/\s+/);
934
+ const head = parts[0];
935
+ if (head.startsWith('./') || head.startsWith('../')) {
936
+ return fileSet.has(normalizeRelativePath(head));
937
+ }
938
+ if (!KNOWN_COMMANDS.has(head)) {
939
+ return false;
940
+ }
941
+ if (head === 'npm' || head === 'pnpm' || head === 'yarn') {
942
+ const scriptMatch = segment.match(/^(npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9:_-]+)\b/);
943
+ if (scriptMatch && !COMMON_PACKAGE_MANAGER_SUBCOMMANDS.has(scriptMatch[2])) {
944
+ return hasPackageScript(scriptMatch[2]);
945
+ }
946
+ return true;
947
+ }
948
+ if ((head === 'npx' || head === 'pnpx') && parts.length > 1) {
949
+ return KNOWN_COMMANDS.has(parts[1]) || hasPackageScript(parts[1]) || fileSet.has(normalizeRelativePath(parts[1]));
950
+ }
951
+ if (head === 'charter')
952
+ return true;
953
+ if (head === 'vitest' || head === 'pytest' || head === 'cargo' || head === 'go')
954
+ return true;
955
+ if (head === 'node' || head === 'python' || head === 'bash' || head === 'sh') {
956
+ if (parts.length === 1)
957
+ return true;
958
+ const target = normalizeRelativePath(parts[1]);
959
+ if (fileSet.has(target))
960
+ return true;
961
+ return !looksLikePath(parts[1]);
962
+ }
963
+ if (head === 'make' || head === 'just')
964
+ return true;
965
+ if (head === packageManager && segment === `${packageManager} test`)
966
+ return true;
967
+ return true;
968
+ }
969
+ const COMMON_PACKAGE_MANAGER_SUBCOMMANDS = new Set([
970
+ 'add',
971
+ 'build',
972
+ 'create',
973
+ 'dev',
974
+ 'dlx',
975
+ 'exec',
976
+ 'install',
977
+ 'lint',
978
+ 'remove',
979
+ 'test',
980
+ 'why',
981
+ ]);
982
+ function hasPackageScript(scriptName) {
983
+ for (const packageJsonPath of ['package.json', 'apps/web/package.json']) {
984
+ if (!fileExists(packageJsonPath))
985
+ continue;
986
+ try {
987
+ const parsed = JSON.parse(fs.readFileSync(path.resolve(packageJsonPath), 'utf-8'));
988
+ if (parsed.scripts && scriptName in parsed.scripts) {
989
+ return true;
990
+ }
991
+ }
992
+ catch {
993
+ // ignore malformed package file
994
+ }
995
+ }
996
+ return false;
997
+ }
998
+ function isTestCommand(command) {
999
+ return /\b(test|tests|vitest|jest|pytest|cargo test|go test|phpunit|tox|playwright test)\b/i.test(command);
1000
+ }
1001
+ function toGrade(score) {
1002
+ if (score >= 90)
1003
+ return 'A';
1004
+ if (score >= 80)
1005
+ return 'B';
1006
+ if (score >= 70)
1007
+ return 'C';
1008
+ if (score >= 60)
1009
+ return 'D';
1010
+ return 'F';
1011
+ }
1012
+ function toCategoryStatus(score, max) {
1013
+ const ratio = max === 0 ? 0 : score / max;
1014
+ if (ratio >= 0.8)
1015
+ return 'strong';
1016
+ if (ratio >= 0.4)
1017
+ return 'partial';
1018
+ return 'weak';
1019
+ }
1020
+ function agentConfigSummary(claudeExists, claudeSubstantive, cursorFiles, cursorSubstantive, agentsFile, agentsSubstantive, manifestExists, manifestPath) {
1021
+ const parts = [];
1022
+ parts.push(claudeExists
1023
+ ? claudeSubstantive ? 'CLAUDE.md is substantive' : 'CLAUDE.md exists but is thin'
1024
+ : 'CLAUDE.md missing');
1025
+ parts.push(cursorFiles.length > 0
1026
+ ? cursorSubstantive ? `${cursorFiles[0]} present` : `${cursorFiles[0]} exists but is thin`
1027
+ : '.cursorrules missing');
1028
+ parts.push(agentsFile
1029
+ ? agentsSubstantive ? `${agentsFile} present` : `${agentsFile} exists but is thin`
1030
+ : 'AGENTS.md missing');
1031
+ parts.push(manifestExists ? `${manifestPath} present` : `${manifestPath} missing`);
1032
+ return parts.join('; ');
1033
+ }
1034
+ function groundingSummary(pathTotal, validPaths, brokenPaths, commandTotal, runnableCommands) {
1035
+ const pathPart = pathTotal > 0
1036
+ ? `${validPaths}/${pathTotal} referenced paths resolve`
1037
+ : 'no file-path grounding found';
1038
+ const commandPart = commandTotal > 0
1039
+ ? `${runnableCommands}/${commandTotal} code-block commands look runnable`
1040
+ : 'no runnable command blocks found';
1041
+ if (brokenPaths > 0) {
1042
+ return `${pathPart}; ${commandPart}; ${brokenPaths} broken reference(s)`;
1043
+ }
1044
+ return `${pathPart}; ${commandPart}`;
1045
+ }
1046
+ function architectureSummary(manifestExists, manifestParsed, existingModules, missingModules, constraintsDefined, stateDefined) {
1047
+ const manifestPart = manifestExists
1048
+ ? manifestParsed ? 'manifest parses cleanly' : 'manifest exists but does not parse'
1049
+ : 'no ADF manifest';
1050
+ const modulePart = existingModules > 0
1051
+ ? `${existingModules} module(s) found${missingModules > 0 ? `, ${missingModules} missing` : ''}`
1052
+ : 'no routed modules found';
1053
+ const constraintsPart = constraintsDefined ? 'constraints defined' : 'constraints missing';
1054
+ const statePart = stateDefined ? 'state tracking present' : 'state tracking missing';
1055
+ return `${manifestPart}; ${modulePart}; ${constraintsPart}; ${statePart}`;
1056
+ }
1057
+ function testingSummary(documented, derived, ciFiles) {
1058
+ const testPart = documented.length > 0
1059
+ ? `${documented.length} documented test command(s)`
1060
+ : derived.length > 0
1061
+ ? `${derived.length} discoverable test command(s), but not documented`
1062
+ : 'no test command found';
1063
+ const ciPart = ciFiles.length > 0
1064
+ ? `CI config present (${ciFiles.join(', ')})`
1065
+ : 'CI config missing';
1066
+ return `${testPart}; ${ciPart}`;
1067
+ }
1068
+ function governanceSummary(skillFiles, permissionFiles, hookFiles) {
1069
+ const skillPart = skillFiles.length > 0 ? `${skillFiles.length} skill file(s)` : 'no skills found';
1070
+ const permissionPart = permissionFiles.length > 0 ? `${permissionFiles.length} permission config file(s)` : 'permissions not explicit';
1071
+ const hookPart = hookFiles.length > 0 ? `${hookFiles.length} hook(s) wired` : 'hooks not wired';
1072
+ return `${skillPart}; ${permissionPart}; ${hookPart}`;
1073
+ }
1074
+ function freshnessSummary(freshness) {
1075
+ if (!freshness.latestCodeChange || !freshness.latestConfigChange) {
1076
+ return 'not enough signal to compare config freshness';
1077
+ }
1078
+ if ((freshness.deltaDays ?? 0) === 0) {
1079
+ return `config is current with code (${freshness.latestConfigChange} vs ${freshness.latestCodeChange})`;
1080
+ }
1081
+ return `config trails code by ${freshness.deltaDays} day(s) (${freshness.latestConfigChange} vs ${freshness.latestCodeChange})`;
1082
+ }
1083
+ function buildRecommendations(input) {
1084
+ const recommendations = [];
1085
+ if (!input.manifestExists) {
1086
+ recommendations.push({
1087
+ priority: 10,
1088
+ text: `Add ${input.manifestPath} and baseline ADF modules with \`charter bootstrap --yes\` so agents get routed repo context.`,
1089
+ });
1090
+ }
1091
+ else if (!input.manifestParsed) {
1092
+ recommendations.push({
1093
+ priority: 9,
1094
+ text: `Fix ${input.manifestPath} so it parses cleanly; broken routing removes architecture signal for agents.`,
1095
+ });
1096
+ }
1097
+ if (!input.claudeExists) {
1098
+ recommendations.push({
1099
+ priority: 8,
1100
+ text: 'Add `CLAUDE.md` with repo-specific entrypoints, test commands, and review constraints.',
1101
+ });
1102
+ }
1103
+ else if (!input.claudeSubstantive) {
1104
+ recommendations.push({
1105
+ priority: 6,
1106
+ text: 'Expand `CLAUDE.md` beyond a thin pointer so it captures the repo-specific commands and guardrails an agent needs on first read.',
1107
+ });
1108
+ }
1109
+ if (input.cursorFiles.length === 0) {
1110
+ recommendations.push({
1111
+ priority: 5,
1112
+ text: 'Add `.cursorrules` or `.cursor/rules/*` so Cursor-class agents inherit the same repo rules.',
1113
+ });
1114
+ }
1115
+ if (!input.agentsFile) {
1116
+ recommendations.push({
1117
+ priority: 5,
1118
+ text: 'Add `AGENTS.md` to document multi-agent handoffs, ownership boundaries, and local operating rules.',
1119
+ });
1120
+ }
1121
+ if (input.brokenPaths.length > 0) {
1122
+ recommendations.push({
1123
+ priority: 8,
1124
+ text: `Fix or remove ${input.brokenPaths.length} broken path reference(s): ${input.brokenPaths.slice(0, 3).join(', ')}${input.brokenPaths.length > 3 ? ', ...' : ''}.`,
1125
+ });
1126
+ }
1127
+ if (input.commandSignals.length === 0) {
1128
+ recommendations.push({
1129
+ priority: 7,
1130
+ text: 'Document at least one runnable setup/test command in `README.md`, `CLAUDE.md`, or your ADF files.',
1131
+ });
1132
+ }
1133
+ else if (input.commandSignals.some((signal) => !signal.runnable)) {
1134
+ recommendations.push({
1135
+ priority: 6,
1136
+ text: 'Replace placeholder or non-runnable command examples with commands that match the repo tooling exactly.',
1137
+ });
1138
+ }
1139
+ if (input.missingModules.length > 0) {
1140
+ recommendations.push({
1141
+ priority: 7,
1142
+ text: `Restore or register missing ADF module(s): ${input.missingModules.slice(0, 3).join(', ')}${input.missingModules.length > 3 ? ', ...' : ''}.`,
1143
+ });
1144
+ }
1145
+ if (!input.constraintsDefined) {
1146
+ recommendations.push({
1147
+ priority: 7,
1148
+ text: `Define non-negotiable constraints in \`${input.aiDir}/core.adf\` or \`CLAUDE.md\` so agents have explicit load-bearing rules.`,
1149
+ });
1150
+ }
1151
+ if (!input.stateDefined) {
1152
+ recommendations.push({
1153
+ priority: 5,
1154
+ text: `Track current state and next steps in \`${input.aiDir}/state.adf\` or a top-level status doc.`,
1155
+ });
1156
+ }
1157
+ if (input.documentedTestCommands.length === 0 && input.derivedTestCommands.length > 0) {
1158
+ recommendations.push({
1159
+ priority: 6,
1160
+ text: `Document the canonical test command for agents to run, for example \`${input.derivedTestCommands[0]}\`.`,
1161
+ });
1162
+ }
1163
+ else if (input.documentedTestCommands.length === 0) {
1164
+ recommendations.push({
1165
+ priority: 6,
1166
+ text: 'Add a runnable test command so agents can verify changes locally before handing work back.',
1167
+ });
1168
+ }
1169
+ if (input.ciFiles.length === 0) {
1170
+ recommendations.push({
1171
+ priority: 6,
1172
+ text: 'Add CI under `.github/workflows/` or your platform equivalent so agent-written changes have an automated gate.',
1173
+ });
1174
+ }
1175
+ if (input.skillFiles.length === 0) {
1176
+ recommendations.push({
1177
+ priority: 4,
1178
+ text: 'Define reusable repo skills with `SKILL.md` files for repeated workflows or specialist tasks.',
1179
+ });
1180
+ }
1181
+ if (input.permissionFiles.length === 0) {
1182
+ recommendations.push({
1183
+ priority: 4,
1184
+ text: 'Add explicit agent permission or sandbox settings under `.claude/`, `.codex/`, `.cursor/`, or equivalent config.',
1185
+ });
1186
+ }
1187
+ if (input.hookFiles.length === 0) {
1188
+ recommendations.push({
1189
+ priority: 4,
1190
+ text: 'Wire `pre-commit` or `commit-msg` hooks with `.husky/`, `.githooks/`, or `charter hook install`.',
1191
+ });
1192
+ }
1193
+ if ((input.freshnessSignals.deltaDays ?? 0) > 30 && input.freshnessSignals.latestCodeChange && input.freshnessSignals.latestConfigChange) {
1194
+ recommendations.push({
1195
+ priority: 7,
1196
+ text: `Refresh agent config after recent code changes; config is last updated on ${input.freshnessSignals.latestConfigChange} while code changed on ${input.freshnessSignals.latestCodeChange}.`,
1197
+ });
1198
+ }
1199
+ return recommendations
1200
+ .sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text))
1201
+ .slice(0, 5)
1202
+ .map((entry) => entry.text);
1203
+ }
1204
+ function printScoreReport(report) {
1205
+ const gradeColor = report.score.grade === 'A' || report.score.grade === 'B'
1206
+ ? 'green'
1207
+ : report.score.grade === 'C' || report.score.grade === 'D'
1208
+ ? 'yellow'
1209
+ : 'red';
1210
+ const labelWidth = Math.max(...report.categories.map((category) => category.label.length));
1211
+ console.log('');
1212
+ console.log(` ${style('Charter Score', 'bold')}`);
1213
+ console.log(` Repo: ${report.repo}`);
1214
+ console.log(` Score: ${style(`${report.score.total}/100 ${report.score.grade}`, gradeColor, 'bold')}`);
1215
+ console.log('');
1216
+ console.log(' Categories');
1217
+ for (const category of report.categories) {
1218
+ const color = category.status === 'strong' ? 'green' : category.status === 'partial' ? 'yellow' : 'red';
1219
+ const icon = category.status === 'strong' ? '[ok]' : category.status === 'partial' ? '[warn]' : '[miss]';
1220
+ console.log(` ${style(icon, color)} ${category.label.padEnd(labelWidth)} ${style(`${String(category.score).padStart(2)}/${category.max}`, color)} ${category.summary}`);
1221
+ }
1222
+ if (report.recommendations.length > 0) {
1223
+ console.log('');
1224
+ console.log(' Recommendations');
1225
+ for (const recommendation of report.recommendations) {
1226
+ console.log(` - ${recommendation}`);
1227
+ }
1228
+ }
1229
+ console.log('');
1230
+ }
1231
+ function printHelp() {
1232
+ console.log('');
1233
+ console.log(' charter score');
1234
+ console.log('');
1235
+ console.log(' Usage:');
1236
+ console.log(' charter score [--ai-dir <dir>] [--format text|json] [--ci]');
1237
+ console.log('');
1238
+ console.log(' Deterministic local AI-readiness audit for the current repo.');
1239
+ console.log(' Scores agent config, grounding, architecture, testing, governance, and freshness.');
1240
+ console.log('');
1241
+ console.log(' --ai-dir <dir>: Override the ADF directory (default: .ai)');
1242
+ console.log(` --ci: exit 1 when score is below ${CI_MIN_SCORE}`);
1243
+ console.log('');
1244
+ }
1245
+ function style(text, ...styles) {
1246
+ if (!supportsColor())
1247
+ return text;
1248
+ const codes = {
1249
+ bold: '\u001b[1m',
1250
+ cyan: '\u001b[36m',
1251
+ dim: '\u001b[2m',
1252
+ green: '\u001b[32m',
1253
+ red: '\u001b[31m',
1254
+ yellow: '\u001b[33m',
1255
+ };
1256
+ return `${styles.map((entry) => codes[entry]).join('')}${text}\u001b[0m`;
1257
+ }
1258
+ function supportsColor() {
1259
+ if (process.env.NO_COLOR)
1260
+ return false;
1261
+ if (process.env.FORCE_COLOR === '0')
1262
+ return false;
1263
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0')
1264
+ return true;
1265
+ return !!process.stdout.isTTY && process.env.TERM !== 'dumb';
1266
+ }
1267
+ function normalizeRelativePath(relativePath) {
1268
+ return relativePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/');
1269
+ }
1270
+ function formatDate(date) {
1271
+ return date.toISOString().slice(0, 10);
1272
+ }
1273
+ //# sourceMappingURL=score.js.map