@aiwg/cli 2026.8.15 → 2026.8.16

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.
@@ -17,30 +17,10 @@ import { DEFAULT_INDEX_EXTENSIONS, INDEX_EXTRACTOR_VERSION, INDEX_VERSION, INDEX
17
17
  import { parseCitationSidecar, citationResultToEdges, buildRefToPathMap } from './citation-parser.js';
18
18
  import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
19
19
  import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-manifest.js';
20
- import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
21
20
  import { normalizeOperationalState } from './operational-state.js';
22
21
  import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
23
22
  import { normalizeStateTransferProjection } from './state-transfer.js';
24
- function pathContains(parent, child) {
25
- const relative = path.relative(parent, child);
26
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
27
- }
28
- function toPosixPath(value) {
29
- return value.split(path.sep).join('/');
30
- }
31
- function indexPathFor(cwd, fullPath, graph) {
32
- if (!graph || graph === 'project') {
33
- const artifactRoot = resolveProjectAiwgDir(cwd);
34
- if (pathContains(artifactRoot, fullPath)) {
35
- const relative = toPosixPath(path.relative(artifactRoot, fullPath));
36
- return relative ? `${DEFAULT_PROJECT_AIWG_DIR}/${relative}` : DEFAULT_PROJECT_AIWG_DIR;
37
- }
38
- }
39
- const rel = path.relative(cwd, fullPath);
40
- if (!rel.startsWith('..') && !path.isAbsolute(rel))
41
- return toPosixPath(rel);
42
- return fullPath;
43
- }
23
+ import { collectGraphIndexFiles, findArtifactFiles, indexPathFor } from './index-files.js';
44
24
  function absoluteEntryPath(cwd, entryPath, graph) {
45
25
  if ((!graph || graph === 'project') && entryPath.startsWith(`${DEFAULT_PROJECT_AIWG_DIR}/`)) {
46
26
  return path.join(resolveProjectAiwgDir(cwd), entryPath.slice(DEFAULT_PROJECT_AIWG_DIR.length + 1));
@@ -587,31 +567,6 @@ function applyMetadataSupplements(entries, supplements, cwd) {
587
567
  }
588
568
  }
589
569
  }
590
- /**
591
- * Recursively find all indexable files under a directory
592
- */
593
- function findArtifactFiles(dir, extensions = [...DEFAULT_INDEX_EXTENSIONS]) {
594
- const results = [];
595
- if (!fs.existsSync(dir))
596
- return results;
597
- const entries = fs.readdirSync(dir, { withFileTypes: true });
598
- for (const entry of entries) {
599
- const fullPath = path.join(dir, entry.name);
600
- if (entry.isSymbolicLink() && !fs.existsSync(fullPath)) {
601
- continue;
602
- }
603
- if (entry.isDirectory()) {
604
- // Skip hidden dirs and .index
605
- if (entry.name.startsWith('.'))
606
- continue;
607
- results.push(...findArtifactFiles(fullPath, extensions));
608
- }
609
- else if (extensions.some(ext => entry.name.endsWith(ext))) {
610
- results.push(fullPath);
611
- }
612
- }
613
- return results;
614
- }
615
570
  /**
616
571
  * Build the artifact index
617
572
  */
@@ -798,24 +753,9 @@ export async function buildIndex(cwd, options = {}) {
798
753
  pruned: 0,
799
754
  };
800
755
  // Collect files from all scan directories
801
- const files = [];
802
- for (const dir of existingDirs) {
803
- files.push(...findArtifactFiles(dir, fileExtensions));
804
- }
805
- // WORKSPACE.md is the root of the project context graph. Index it and its
806
- // local Markdown-linked nodes without copying them into provider trees.
807
- if (!scope && (!graph || graph === 'project')) {
808
- const workspacePath = path.join(cwd, 'WORKSPACE.md');
809
- const contextFiles = [
810
- ...(fs.existsSync(workspacePath) ? [workspacePath] : []),
811
- ...await workspaceLinkedFiles(cwd),
812
- ];
813
- for (const contextFile of contextFiles) {
814
- if (fileExtensions.some((extension) => contextFile.endsWith(extension)) && !files.includes(contextFile)) {
815
- files.push(contextFile);
816
- }
817
- }
818
- }
756
+ const files = scope
757
+ ? existingDirs.flatMap(dir => findArtifactFiles(dir, fileExtensions))
758
+ : await collectGraphIndexFiles(cwd, graph);
819
759
  const entries = {};
820
760
  const tagIndex = {};
821
761
  const depGraph = {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared artifact source-file enumeration.
3
+ *
4
+ * Index builds and coverage reporting must use the same file set so project
5
+ * context files cannot inflate the indexed count beyond the reported total.
6
+ *
7
+ * @implements jmagly/aiwg#146
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { DEFAULT_INDEX_EXTENSIONS, GRAPH_CONFIGS, resolveGraphScanDir, } from './types.js';
12
+ import { DEFAULT_PROJECT_AIWG_DIR, resolveProjectAiwgDir, } from '../config/project-artifacts.js';
13
+ import { workspaceLinkedFiles } from '../smiths/context-pipeline/workspace-context.js';
14
+ function pathContains(parent, child) {
15
+ const relative = path.relative(parent, child);
16
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
17
+ }
18
+ function toPosixPath(value) {
19
+ return value.split(path.sep).join('/');
20
+ }
21
+ export function indexPathFor(cwd, fullPath, graph) {
22
+ if (!graph || graph === 'project') {
23
+ const artifactRoot = resolveProjectAiwgDir(cwd);
24
+ if (pathContains(artifactRoot, fullPath)) {
25
+ const relative = toPosixPath(path.relative(artifactRoot, fullPath));
26
+ return relative ? `${DEFAULT_PROJECT_AIWG_DIR}/${relative}` : DEFAULT_PROJECT_AIWG_DIR;
27
+ }
28
+ }
29
+ const relative = path.relative(cwd, fullPath);
30
+ if (!relative.startsWith('..') && !path.isAbsolute(relative))
31
+ return toPosixPath(relative);
32
+ return fullPath;
33
+ }
34
+ /** Recursively find indexable files, excluding hidden directories such as .index. */
35
+ export function findArtifactFiles(dir, extensions = DEFAULT_INDEX_EXTENSIONS) {
36
+ const results = [];
37
+ if (!fs.existsSync(dir))
38
+ return results;
39
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ const fullPath = path.join(dir, entry.name);
42
+ if (entry.isSymbolicLink() && !fs.existsSync(fullPath))
43
+ continue;
44
+ if (entry.isDirectory()) {
45
+ if (entry.name.startsWith('.'))
46
+ continue;
47
+ results.push(...findArtifactFiles(fullPath, extensions));
48
+ }
49
+ else if (extensions.some(extension => entry.name.endsWith(extension))) {
50
+ results.push(fullPath);
51
+ }
52
+ }
53
+ return results;
54
+ }
55
+ /** Return the exact current source-file set used by a standard graph build. */
56
+ export async function collectGraphIndexFiles(cwd, graph) {
57
+ const config = graph ? GRAPH_CONFIGS[graph] : undefined;
58
+ const scanDirs = config
59
+ ? config.scanDirs.map(directory => resolveGraphScanDir(cwd, directory))
60
+ : [resolveProjectAiwgDir(cwd)];
61
+ const extensions = config?.extensions ?? [...DEFAULT_INDEX_EXTENSIONS];
62
+ const files = new Set();
63
+ for (const scanDir of scanDirs) {
64
+ for (const file of findArtifactFiles(scanDir, extensions))
65
+ files.add(file);
66
+ }
67
+ if (!graph || graph === 'project') {
68
+ const workspacePath = path.join(cwd, 'WORKSPACE.md');
69
+ const contextFiles = [
70
+ ...(fs.existsSync(workspacePath) ? [workspacePath] : []),
71
+ ...await workspaceLinkedFiles(cwd),
72
+ ];
73
+ for (const contextFile of contextFiles) {
74
+ if (extensions.some(extension => contextFile.endsWith(extension)))
75
+ files.add(contextFile);
76
+ }
77
+ }
78
+ return [...files];
79
+ }
80
+ //# sourceMappingURL=index-files.js.map
@@ -7,40 +7,23 @@
7
7
  * @source @src/artifacts/types.ts
8
8
  * @tests @test/unit/artifacts/stats.test.ts
9
9
  */
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { GRAPH_CONFIGS, loadUserGraphConfigs, resolveGraphScanDir } from './types.js';
10
+ import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
13
11
  import { loadIndexStats, loadGraphIndexFile } from './index-reader.js';
14
- /**
15
- * Count total indexable files under scan directories (excluding .index/)
16
- */
17
- function countArtifactFiles(cwd, graphType) {
18
- const config = graphType ? GRAPH_CONFIGS[graphType] : undefined;
19
- const scanDirs = config
20
- ? config.scanDirs.map(d => resolveGraphScanDir(cwd, d))
21
- : [resolveGraphScanDir(cwd, '.aiwg')];
22
- const extensions = config?.extensions ?? ['.md', '.yaml', '.json'];
23
- let count = 0;
24
- function walk(dir) {
25
- if (!fs.existsSync(dir))
26
- return;
27
- const entries = fs.readdirSync(dir, { withFileTypes: true });
28
- for (const entry of entries) {
29
- const full = path.join(dir, entry.name);
30
- if (entry.isDirectory()) {
31
- if (entry.name.startsWith('.'))
32
- continue; // Skip .index, etc.
33
- walk(full);
34
- }
35
- else if (extensions.some(ext => entry.name.endsWith(ext))) {
36
- count++;
37
- }
38
- }
39
- }
40
- for (const dir of scanDirs) {
41
- walk(dir);
42
- }
43
- return count;
12
+ import { collectGraphIndexFiles, indexPathFor } from './index-files.js';
13
+ /** Calculate coverage over the same current file set used by the index builder. */
14
+ async function calculateCoverage(cwd, stats, graphType) {
15
+ const sourcePaths = new Set((await collectGraphIndexFiles(cwd, graphType))
16
+ .map(file => indexPathFor(cwd, file, graphType)));
17
+ const index = loadGraphIndexFile(cwd, 'metadata.json', graphType);
18
+ const indexed = index
19
+ ? Object.keys(index.entries).filter(entryPath => sourcePaths.has(entryPath)).length
20
+ : Math.min(stats.totalArtifacts, sourcePaths.size);
21
+ const totalFiles = sourcePaths.size;
22
+ return {
23
+ indexed,
24
+ totalFiles,
25
+ percentage: totalFiles > 0 ? Math.round((indexed / totalFiles) * 100) : 100,
26
+ };
44
27
  }
45
28
  /**
46
29
  * Show artifact index statistics
@@ -84,14 +67,10 @@ export async function showStats(cwd, options = {}) {
84
67
  // JSON mode: aggregate all graphs into one response
85
68
  const combined = {};
86
69
  for (const { type, stats: s } of availableGraphs) {
87
- const totalFiles = countArtifactFiles(cwd, type);
70
+ const coverage = await calculateCoverage(cwd, s, type);
88
71
  combined[type] = {
89
72
  ...s,
90
- coverage: {
91
- indexed: s.totalArtifacts,
92
- totalFiles,
93
- percentage: totalFiles > 0 ? Math.round((s.totalArtifacts / totalFiles) * 100) : 100,
94
- },
73
+ coverage,
95
74
  };
96
75
  }
97
76
  console.log(JSON.stringify(combined, null, 2));
@@ -108,14 +87,10 @@ export async function showStats(cwd, options = {}) {
108
87
  */
109
88
  async function renderStats(cwd, stats, options, graphType) {
110
89
  if (options.json) {
111
- const totalFiles = countArtifactFiles(cwd, graphType);
90
+ const coverage = await calculateCoverage(cwd, stats, graphType);
112
91
  console.log(JSON.stringify({
113
92
  ...stats,
114
- coverage: {
115
- indexed: stats.totalArtifacts,
116
- totalFiles,
117
- percentage: totalFiles > 0 ? Math.round((stats.totalArtifacts / totalFiles) * 100) : 100,
118
- },
93
+ coverage,
119
94
  }, null, 2));
120
95
  return;
121
96
  }
@@ -167,11 +142,8 @@ async function renderStats(cwd, stats, options, graphType) {
167
142
  }
168
143
  console.log('');
169
144
  // Coverage
170
- const totalFiles = countArtifactFiles(cwd, graphType);
171
- const coverage = totalFiles > 0
172
- ? Math.round((stats.totalArtifacts / totalFiles) * 100)
173
- : 100;
145
+ const coverage = await calculateCoverage(cwd, stats, graphType);
174
146
  console.log('Index Health:');
175
- console.log(` Coverage: ${stats.totalArtifacts}/${totalFiles} artifacts indexed (${coverage}%)`);
147
+ console.log(` Coverage: ${coverage.indexed}/${coverage.totalFiles} artifacts indexed (${coverage.percentage}%)`);
176
148
  }
177
149
  //# sourceMappingURL=stats.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.15",
3
+ "version": "2026.8.16",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",