cap-pro 1.0.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.
- package/.claude-plugin/README.md +26 -0
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +806 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-architect.md +269 -0
- package/agents/cap-brainstormer.md +207 -0
- package/agents/cap-curator.md +276 -0
- package/agents/cap-debugger.md +365 -0
- package/agents/cap-designer.md +246 -0
- package/agents/cap-historian.md +464 -0
- package/agents/cap-migrator.md +291 -0
- package/agents/cap-prototyper.md +197 -0
- package/agents/cap-validator.md +308 -0
- package/bin/install.js +5433 -0
- package/cap/bin/cap-tools.cjs +853 -0
- package/cap/bin/lib/arc-scanner.cjs +344 -0
- package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
- package/cap/bin/lib/cap-anchor.cjs +228 -0
- package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
- package/cap/bin/lib/cap-checkpoint.cjs +434 -0
- package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
- package/cap/bin/lib/cap-cluster-display.cjs +52 -0
- package/cap/bin/lib/cap-cluster-format.cjs +245 -0
- package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
- package/cap/bin/lib/cap-cluster-io.cjs +212 -0
- package/cap/bin/lib/cap-completeness.cjs +540 -0
- package/cap/bin/lib/cap-deps.cjs +583 -0
- package/cap/bin/lib/cap-design-families.cjs +332 -0
- package/cap/bin/lib/cap-design.cjs +966 -0
- package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
- package/cap/bin/lib/cap-doctor.cjs +752 -0
- package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
- package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
- package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
- package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
- package/cap/bin/lib/cap-feature-map.cjs +1943 -0
- package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
- package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
- package/cap/bin/lib/cap-learn-review.cjs +1072 -0
- package/cap/bin/lib/cap-learning-signals.cjs +627 -0
- package/cap/bin/lib/cap-loader.cjs +227 -0
- package/cap/bin/lib/cap-logger.cjs +57 -0
- package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
- package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
- package/cap/bin/lib/cap-memory-dir.cjs +987 -0
- package/cap/bin/lib/cap-memory-engine.cjs +698 -0
- package/cap/bin/lib/cap-memory-extends.cjs +398 -0
- package/cap/bin/lib/cap-memory-graph.cjs +790 -0
- package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
- package/cap/bin/lib/cap-memory-pin.cjs +183 -0
- package/cap/bin/lib/cap-memory-platform.cjs +490 -0
- package/cap/bin/lib/cap-memory-prune.cjs +707 -0
- package/cap/bin/lib/cap-memory-schema.cjs +812 -0
- package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
- package/cap/bin/lib/cap-migrate.cjs +540 -0
- package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
- package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
- package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
- package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
- package/cap/bin/lib/cap-reconcile.cjs +570 -0
- package/cap/bin/lib/cap-research-gate.cjs +218 -0
- package/cap/bin/lib/cap-scope-filter.cjs +402 -0
- package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
- package/cap/bin/lib/cap-session-extract.cjs +987 -0
- package/cap/bin/lib/cap-session.cjs +445 -0
- package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
- package/cap/bin/lib/cap-stack-docs.cjs +646 -0
- package/cap/bin/lib/cap-tag-observer.cjs +371 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
- package/cap/bin/lib/cap-telemetry.cjs +466 -0
- package/cap/bin/lib/cap-test-audit.cjs +1438 -0
- package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
- package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
- package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
- package/cap/bin/lib/cap-trace.cjs +399 -0
- package/cap/bin/lib/cap-trust-mode.cjs +336 -0
- package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
- package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
- package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
- package/cap/bin/lib/cap-ui.cjs +1245 -0
- package/cap/bin/lib/cap-upgrade.cjs +1028 -0
- package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
- package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
- package/cap/bin/lib/cli/init-router.cjs +68 -0
- package/cap/bin/lib/cli/phase-router.cjs +102 -0
- package/cap/bin/lib/cli/state-router.cjs +61 -0
- package/cap/bin/lib/cli/template-router.cjs +37 -0
- package/cap/bin/lib/cli/uat-router.cjs +29 -0
- package/cap/bin/lib/cli/validation-router.cjs +26 -0
- package/cap/bin/lib/cli/verification-router.cjs +31 -0
- package/cap/bin/lib/cli/workstream-router.cjs +39 -0
- package/cap/bin/lib/commands.cjs +961 -0
- package/cap/bin/lib/config.cjs +467 -0
- package/cap/bin/lib/convention-reader.cjs +258 -0
- package/cap/bin/lib/core.cjs +1241 -0
- package/cap/bin/lib/feature-aggregator.cjs +423 -0
- package/cap/bin/lib/frontmatter.cjs +337 -0
- package/cap/bin/lib/init.cjs +1443 -0
- package/cap/bin/lib/manifest-generator.cjs +383 -0
- package/cap/bin/lib/milestone.cjs +253 -0
- package/cap/bin/lib/model-profiles.cjs +69 -0
- package/cap/bin/lib/monorepo-context.cjs +226 -0
- package/cap/bin/lib/monorepo-migrator.cjs +509 -0
- package/cap/bin/lib/phase.cjs +889 -0
- package/cap/bin/lib/profile-output.cjs +989 -0
- package/cap/bin/lib/profile-pipeline.cjs +540 -0
- package/cap/bin/lib/roadmap.cjs +330 -0
- package/cap/bin/lib/security.cjs +394 -0
- package/cap/bin/lib/session-manager.cjs +292 -0
- package/cap/bin/lib/skeleton-generator.cjs +179 -0
- package/cap/bin/lib/state.cjs +1032 -0
- package/cap/bin/lib/template.cjs +231 -0
- package/cap/bin/lib/test-detector.cjs +62 -0
- package/cap/bin/lib/uat.cjs +283 -0
- package/cap/bin/lib/verify.cjs +889 -0
- package/cap/bin/lib/workspace-detector.cjs +371 -0
- package/cap/bin/lib/workstream.cjs +492 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +101 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/contract-test-templates.md +312 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profiles.md +174 -0
- package/cap/references/phase-numbering.md +126 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/property-test-templates.md +316 -0
- package/cap/references/security-test-templates.md +347 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/claude-md.md +175 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary.md +364 -0
- package/cap/templates/user-preferences.md +498 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +857 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +449 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +298 -0
- package/cap/workflows/ui-review.md +161 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +170 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +393 -0
- package/commands/cap/checkpoint.md +106 -0
- package/commands/cap/completeness.md +94 -0
- package/commands/cap/continue.md +72 -0
- package/commands/cap/debug.md +588 -0
- package/commands/cap/deps.md +169 -0
- package/commands/cap/design.md +479 -0
- package/commands/cap/init.md +354 -0
- package/commands/cap/iterate.md +249 -0
- package/commands/cap/learn.md +459 -0
- package/commands/cap/memory.md +275 -0
- package/commands/cap/migrate-feature-map.md +91 -0
- package/commands/cap/migrate-memory.md +108 -0
- package/commands/cap/migrate-tags.md +91 -0
- package/commands/cap/migrate.md +131 -0
- package/commands/cap/prototype.md +510 -0
- package/commands/cap/reconcile.md +121 -0
- package/commands/cap/review.md +360 -0
- package/commands/cap/save.md +72 -0
- package/commands/cap/scan.md +404 -0
- package/commands/cap/start.md +356 -0
- package/commands/cap/status.md +118 -0
- package/commands/cap/test-audit.md +262 -0
- package/commands/cap/test.md +394 -0
- package/commands/cap/trace.md +133 -0
- package/commands/cap/ui.md +167 -0
- package/hooks/dist/cap-check-update.js +115 -0
- package/hooks/dist/cap-context-monitor.js +185 -0
- package/hooks/dist/cap-learn-review-hook.js +114 -0
- package/hooks/dist/cap-learning-hook.js +192 -0
- package/hooks/dist/cap-memory.js +299 -0
- package/hooks/dist/cap-prompt-guard.js +97 -0
- package/hooks/dist/cap-statusline.js +157 -0
- package/hooks/dist/cap-tag-observer.js +115 -0
- package/hooks/dist/cap-version-check.js +112 -0
- package/hooks/dist/cap-workflow-guard.js +175 -0
- package/hooks/hooks.json +55 -0
- package/package.json +58 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +93 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +199 -0
- package/scripts/run-tests.cjs +181 -0
- package/scripts/secret-scan.sh +227 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// @cap-feature(feature:F-048, primary:true) Implementation Completeness Score — 4-point per-AC audit.
|
|
4
|
+
// Computes four independent signals per acceptance criterion:
|
|
5
|
+
// (a) tag exists in code — @cap-todo/@cap-feature tag references this AC
|
|
6
|
+
// (b) test exists referencing it — tagged test file (tests/** or *.test.*) also references this AC
|
|
7
|
+
// (c) test invokes tagged code — test's static imports reach the primary implementation file
|
|
8
|
+
// (d) reachable from public — primary file is reachable via imports from bin/install.js or hooks/
|
|
9
|
+
//
|
|
10
|
+
// Each signal is 0 or 1. The sum is the completeness score (0..4). A feature's
|
|
11
|
+
// average is the mean of its AC scores.
|
|
12
|
+
//
|
|
13
|
+
// @cap-decision Pure computation. scoreAc() takes a pre-computed context and returns
|
|
14
|
+
// a structured result. The only I/O surface is buildContext() (scans the project once)
|
|
15
|
+
// and loadCompletenessConfig() (reads .cap/config.json). Performance-critical code paths
|
|
16
|
+
// reuse the context across all ACs — expected wall-clock <5s for 100 features.
|
|
17
|
+
// @cap-decision Reachability uses static CJS/ESM imports only. Dynamic requires, runtime
|
|
18
|
+
// plugin loading, and command-markdown references are NOT followed — these are documented
|
|
19
|
+
// limitations consistent with F-049's constraints.
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const deps = require('./cap-deps.cjs');
|
|
24
|
+
|
|
25
|
+
const CONFIG_FILE = path.join('.cap', 'config.json');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONFIG = {
|
|
28
|
+
enabled: false,
|
|
29
|
+
shipThreshold: 3.5,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const TEST_FILE_PATTERNS = [
|
|
33
|
+
/\.test\.[cm]?js$/i,
|
|
34
|
+
/\.test\.tsx?$/i,
|
|
35
|
+
/\.spec\.[cm]?js$/i,
|
|
36
|
+
/\.spec\.tsx?$/i,
|
|
37
|
+
/^tests?\//, // path starts with tests/ or test/
|
|
38
|
+
/\/tests?\//,
|
|
39
|
+
/^__tests__\//, // Jest convention
|
|
40
|
+
/\/__tests__\//,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} CompletenessConfig
|
|
45
|
+
* @property {boolean} enabled
|
|
46
|
+
* @property {number} shipThreshold
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} CompletenessContext
|
|
51
|
+
* @property {Object} featureMap - Output of readFeatureMap()
|
|
52
|
+
* @property {Array} tags - Output of scanner.scanDirectory()
|
|
53
|
+
* @property {Object} acFileMap - Output of scanner.buildAcFileMap()
|
|
54
|
+
* @property {Map<string,string>} fileToFeature - from cap-deps
|
|
55
|
+
* @property {Set<string>} publicReachable - absolute paths reachable from public surface
|
|
56
|
+
* @property {Map<string,Array>} importsByFile - absolute path -> ImportSpec[] (cached)
|
|
57
|
+
* @property {string} projectRoot
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} AcScore
|
|
62
|
+
* @property {string} acRef - 'F-XXX/AC-N'
|
|
63
|
+
* @property {Object} signals
|
|
64
|
+
* @property {boolean} signals.tag
|
|
65
|
+
* @property {boolean} signals.test
|
|
66
|
+
* @property {boolean} signals.testInvokesCode
|
|
67
|
+
* @property {boolean} signals.reachable
|
|
68
|
+
* @property {number} score - 0..4
|
|
69
|
+
* @property {string[]} reasons - short strings explaining each signal's outcome
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} FeatureScore
|
|
74
|
+
* @property {string} featureId
|
|
75
|
+
* @property {string} state - lifecycle state from FEATURE-MAP
|
|
76
|
+
* @property {AcScore[]} acs
|
|
77
|
+
* @property {number} averageScore - arithmetic mean, 0..4, or NaN when feature has no ACs
|
|
78
|
+
* @property {number} acCount
|
|
79
|
+
* @property {number} scoreSum
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check whether a file path looks like a test file.
|
|
84
|
+
* @param {string} filePath - Relative or absolute
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isTestFile(filePath) {
|
|
88
|
+
if (!filePath || typeof filePath !== 'string') return false;
|
|
89
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
90
|
+
return TEST_FILE_PATTERNS.some((re) => re.test(normalized));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Score a single AC against a pre-computed context.
|
|
95
|
+
* @param {string} acRef - 'F-XXX/AC-N'
|
|
96
|
+
* @param {CompletenessContext} ctx
|
|
97
|
+
* @returns {AcScore}
|
|
98
|
+
*/
|
|
99
|
+
function scoreAc(acRef, ctx) {
|
|
100
|
+
const entry = ctx.acFileMap[acRef];
|
|
101
|
+
const files = (entry && entry.files) || [];
|
|
102
|
+
const primary = (entry && entry.primary) || null;
|
|
103
|
+
|
|
104
|
+
const reasons = [];
|
|
105
|
+
|
|
106
|
+
// -------- Signal (a): tag exists in (non-test) code --------
|
|
107
|
+
const codeFiles = files.filter((f) => !isTestFile(f));
|
|
108
|
+
const tagSignal = codeFiles.length > 0;
|
|
109
|
+
reasons.push(
|
|
110
|
+
tagSignal
|
|
111
|
+
? `tag: ${codeFiles.length} file(s) tagged`
|
|
112
|
+
: 'tag: no @cap-* tag references this AC in source files'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// -------- Signal (b): test exists referencing the AC --------
|
|
116
|
+
const testFiles = files.filter((f) => isTestFile(f));
|
|
117
|
+
const testSignal = testFiles.length > 0;
|
|
118
|
+
reasons.push(
|
|
119
|
+
testSignal
|
|
120
|
+
? `test: ${testFiles.length} test file(s) tag this AC`
|
|
121
|
+
: 'test: no test file has a @cap-* tag referencing this AC'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// -------- Signal (c): test invokes the tagged code --------
|
|
125
|
+
let testInvokesCode = false;
|
|
126
|
+
if (testSignal && primary) {
|
|
127
|
+
const primaryAbs = path.isAbsolute(primary)
|
|
128
|
+
? primary
|
|
129
|
+
: path.resolve(ctx.projectRoot, primary);
|
|
130
|
+
for (const tf of testFiles) {
|
|
131
|
+
const testAbs = path.isAbsolute(tf) ? tf : path.resolve(ctx.projectRoot, tf);
|
|
132
|
+
if (testReachesFile(testAbs, primaryAbs, ctx, /* maxDepth */ 3)) {
|
|
133
|
+
testInvokesCode = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
reasons.push(
|
|
139
|
+
testInvokesCode
|
|
140
|
+
? 'invokes: at least one test imports the primary file (static graph)'
|
|
141
|
+
: testSignal
|
|
142
|
+
? 'invokes: test does not import the primary file within 3 hops'
|
|
143
|
+
: 'invokes: skipped (no test present)'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// -------- Signal (d): tagged code reachable from public surface --------
|
|
147
|
+
let reachable = false;
|
|
148
|
+
if (primary) {
|
|
149
|
+
const primaryAbs = path.isAbsolute(primary)
|
|
150
|
+
? primary
|
|
151
|
+
: path.resolve(ctx.projectRoot, primary);
|
|
152
|
+
reachable = ctx.publicReachable.has(primaryAbs);
|
|
153
|
+
}
|
|
154
|
+
reasons.push(
|
|
155
|
+
reachable
|
|
156
|
+
? 'reachable: primary file is imported from public surface (bin/install.js, hooks/)'
|
|
157
|
+
: 'reachable: primary file not reachable from bin/install.js or hooks/'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const score =
|
|
161
|
+
(tagSignal ? 1 : 0) +
|
|
162
|
+
(testSignal ? 1 : 0) +
|
|
163
|
+
(testInvokesCode ? 1 : 0) +
|
|
164
|
+
(reachable ? 1 : 0);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
acRef,
|
|
168
|
+
signals: {
|
|
169
|
+
tag: tagSignal,
|
|
170
|
+
test: testSignal,
|
|
171
|
+
testInvokesCode,
|
|
172
|
+
reachable,
|
|
173
|
+
},
|
|
174
|
+
score,
|
|
175
|
+
reasons,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* BFS over static imports from `startFile` looking for `targetFile`.
|
|
181
|
+
* Caches import lists per-file via ctx.importsByFile.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} startFile - absolute
|
|
184
|
+
* @param {string} targetFile - absolute
|
|
185
|
+
* @param {CompletenessContext} ctx
|
|
186
|
+
* @param {number} maxDepth
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
function testReachesFile(startFile, targetFile, ctx, maxDepth) {
|
|
190
|
+
if (startFile === targetFile) return true;
|
|
191
|
+
const queue = [{ file: startFile, depth: 0 }];
|
|
192
|
+
const seen = new Set([startFile]);
|
|
193
|
+
while (queue.length > 0) {
|
|
194
|
+
const { file, depth } = queue.shift();
|
|
195
|
+
if (depth >= maxDepth) continue;
|
|
196
|
+
let imports = ctx.importsByFile.get(file);
|
|
197
|
+
if (imports === undefined) {
|
|
198
|
+
try {
|
|
199
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
200
|
+
imports = deps.parseImports(content);
|
|
201
|
+
} catch (_e) {
|
|
202
|
+
imports = [];
|
|
203
|
+
}
|
|
204
|
+
ctx.importsByFile.set(file, imports);
|
|
205
|
+
}
|
|
206
|
+
for (const imp of imports) {
|
|
207
|
+
const resolved = deps.resolveImportToFile(imp.source, file);
|
|
208
|
+
if (!resolved) continue;
|
|
209
|
+
if (resolved === targetFile) return true;
|
|
210
|
+
if (!seen.has(resolved)) {
|
|
211
|
+
seen.add(resolved);
|
|
212
|
+
queue.push({ file: resolved, depth: depth + 1 });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Compute reachability set from public surface files (bin/install.js + hooks/*.js)
|
|
221
|
+
* outward via static imports. Returns absolute paths of all reachable files.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} projectRoot
|
|
224
|
+
* @returns {Set<string>}
|
|
225
|
+
*/
|
|
226
|
+
function computePublicReachable(projectRoot) {
|
|
227
|
+
const roots = collectPublicSurfaceFiles(projectRoot);
|
|
228
|
+
const reachable = new Set();
|
|
229
|
+
const queue = [];
|
|
230
|
+
for (const r of roots) {
|
|
231
|
+
reachable.add(r);
|
|
232
|
+
queue.push(r);
|
|
233
|
+
}
|
|
234
|
+
while (queue.length > 0) {
|
|
235
|
+
const file = queue.shift();
|
|
236
|
+
let content;
|
|
237
|
+
try {
|
|
238
|
+
content = fs.readFileSync(file, 'utf8');
|
|
239
|
+
} catch (_e) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const imports = deps.parseImports(content);
|
|
243
|
+
for (const imp of imports) {
|
|
244
|
+
const resolved = deps.resolveImportToFile(imp.source, file);
|
|
245
|
+
if (!resolved) continue;
|
|
246
|
+
if (!reachable.has(resolved)) {
|
|
247
|
+
reachable.add(resolved);
|
|
248
|
+
queue.push(resolved);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return reachable;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Collect the public-surface entry points. Conservative set: package.json "bin"
|
|
257
|
+
* entries plus any *.js files under hooks/. Returns absolute paths.
|
|
258
|
+
* @param {string} projectRoot
|
|
259
|
+
* @returns {string[]}
|
|
260
|
+
*/
|
|
261
|
+
function collectPublicSurfaceFiles(projectRoot) {
|
|
262
|
+
const entries = [];
|
|
263
|
+
// Defense-in-depth: resolved paths must stay inside projectRoot. A malicious
|
|
264
|
+
// package.json with bin: "../../evil.js" already owns the process (require
|
|
265
|
+
// would execute it anyway), but we don't want completeness reachability to
|
|
266
|
+
// follow pointers outside the project.
|
|
267
|
+
const rootPrefix = path.resolve(projectRoot) + path.sep;
|
|
268
|
+
const pushIfInRoot = (p) => {
|
|
269
|
+
const abs = path.resolve(projectRoot, p);
|
|
270
|
+
if (abs.startsWith(rootPrefix)) entries.push(abs);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// package.json bin entries
|
|
274
|
+
try {
|
|
275
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
276
|
+
const bin = pkg.bin;
|
|
277
|
+
if (typeof bin === 'string') {
|
|
278
|
+
pushIfInRoot(bin);
|
|
279
|
+
} else if (bin && typeof bin === 'object') {
|
|
280
|
+
for (const v of Object.values(bin)) pushIfInRoot(v);
|
|
281
|
+
}
|
|
282
|
+
} catch (_e) { /* no package.json */ }
|
|
283
|
+
|
|
284
|
+
// hooks/*.js
|
|
285
|
+
const hooksDir = path.join(projectRoot, 'hooks');
|
|
286
|
+
try {
|
|
287
|
+
const files = fs.readdirSync(hooksDir);
|
|
288
|
+
for (const f of files) {
|
|
289
|
+
if (f.endsWith('.js') || f.endsWith('.cjs') || f.endsWith('.mjs')) {
|
|
290
|
+
const full = path.join(hooksDir, f);
|
|
291
|
+
const st = fs.statSync(full);
|
|
292
|
+
if (st.isFile()) entries.push(full);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (_e) { /* no hooks dir */ }
|
|
296
|
+
|
|
297
|
+
return entries;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Build the CompletenessContext used by all scoring functions.
|
|
302
|
+
* @param {string} projectRoot
|
|
303
|
+
* @param {{ scanner?: any, featureMap?: any }} [injected] - Optional pre-computed inputs (for tests)
|
|
304
|
+
* @returns {CompletenessContext}
|
|
305
|
+
*/
|
|
306
|
+
function buildContext(projectRoot, injected) {
|
|
307
|
+
const scanner = (injected && injected.scanner) || require('./cap-tag-scanner.cjs');
|
|
308
|
+
const fm = (injected && injected.featureMapModule) || require('./cap-feature-map.cjs');
|
|
309
|
+
const deps = require('./cap-deps.cjs');
|
|
310
|
+
|
|
311
|
+
// @cap-todo(ac:F-081/AC-4 iter:2) Migrated to {safe: true} opt-in to preserve CLI on duplicate-ID FEATURE-MAP.
|
|
312
|
+
// @cap-decision(F-081/iter2) Warn on parseError; continue with partial map for read-only display.
|
|
313
|
+
let featureMap = (injected && injected.featureMap) || fm.readFeatureMap(projectRoot, undefined, { safe: true });
|
|
314
|
+
if (featureMap && featureMap.parseError) {
|
|
315
|
+
console.warn('cap: completeness — duplicate feature ID detected, scoring uses partial map: ' + String(featureMap.parseError.message).trim());
|
|
316
|
+
}
|
|
317
|
+
const tags = (injected && injected.tags) || scanner.scanDirectory(projectRoot);
|
|
318
|
+
const acFileMap = scanner.buildAcFileMap(tags);
|
|
319
|
+
const fileToFeature = deps.buildFileToFeatureMap(tags, projectRoot);
|
|
320
|
+
const publicReachable =
|
|
321
|
+
(injected && injected.publicReachable) || computePublicReachable(projectRoot);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
featureMap,
|
|
325
|
+
tags,
|
|
326
|
+
acFileMap,
|
|
327
|
+
fileToFeature,
|
|
328
|
+
publicReachable,
|
|
329
|
+
importsByFile: new Map(),
|
|
330
|
+
projectRoot,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Score every AC in every feature of the Feature Map.
|
|
336
|
+
* @param {CompletenessContext} ctx
|
|
337
|
+
* @returns {FeatureScore[]}
|
|
338
|
+
*/
|
|
339
|
+
function scoreAllFeatures(ctx) {
|
|
340
|
+
const out = [];
|
|
341
|
+
const features = (ctx.featureMap && ctx.featureMap.features) || [];
|
|
342
|
+
for (const f of features) {
|
|
343
|
+
out.push(scoreFeature(f, ctx));
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Score a single feature.
|
|
350
|
+
* @param {Object} feature - A feature entry from readFeatureMap()
|
|
351
|
+
* @param {CompletenessContext} ctx
|
|
352
|
+
* @returns {FeatureScore}
|
|
353
|
+
*/
|
|
354
|
+
function scoreFeature(feature, ctx) {
|
|
355
|
+
const acs = (feature.acs || []).map((ac) => {
|
|
356
|
+
const acRef = `${feature.id}/${ac.id}`;
|
|
357
|
+
return scoreAc(acRef, ctx);
|
|
358
|
+
});
|
|
359
|
+
const scoreSum = acs.reduce((sum, a) => sum + a.score, 0);
|
|
360
|
+
const averageScore = acs.length > 0 ? scoreSum / acs.length : NaN;
|
|
361
|
+
return {
|
|
362
|
+
featureId: feature.id,
|
|
363
|
+
state: feature.state || null,
|
|
364
|
+
acs,
|
|
365
|
+
averageScore,
|
|
366
|
+
acCount: acs.length,
|
|
367
|
+
scoreSum,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Format a terse per-feature breakdown suitable for `/cap:status --completeness`.
|
|
373
|
+
* @param {FeatureScore[]} scores
|
|
374
|
+
* @returns {string}
|
|
375
|
+
*/
|
|
376
|
+
function formatFeatureBreakdown(scores) {
|
|
377
|
+
if (!Array.isArray(scores) || scores.length === 0) {
|
|
378
|
+
return 'No features to score.';
|
|
379
|
+
}
|
|
380
|
+
const lines = ['Completeness Score (per feature — avg of 4-point AC signals)'];
|
|
381
|
+
lines.push('');
|
|
382
|
+
for (const s of scores) {
|
|
383
|
+
const avg = Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—';
|
|
384
|
+
lines.push(`${s.featureId} [${s.state || '?'}] avg=${avg}/4 (${s.acCount} AC)`);
|
|
385
|
+
for (const ac of s.acs) {
|
|
386
|
+
const flags = [
|
|
387
|
+
ac.signals.tag ? 'T' : '·',
|
|
388
|
+
ac.signals.test ? 'S' : '·',
|
|
389
|
+
ac.signals.testInvokesCode ? 'I' : '·',
|
|
390
|
+
ac.signals.reachable ? 'R' : '·',
|
|
391
|
+
].join('');
|
|
392
|
+
lines.push(` ${ac.acRef.padEnd(14)} ${flags} score=${ac.score}/4`);
|
|
393
|
+
}
|
|
394
|
+
lines.push('');
|
|
395
|
+
}
|
|
396
|
+
lines.push('Legend: T=tagged S=tested I=test-invokes-code R=reachable-from-public');
|
|
397
|
+
return lines.join('\n');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Format a full markdown audit report suitable for PR attachment.
|
|
402
|
+
* @param {FeatureScore[]} scores
|
|
403
|
+
* @returns {string}
|
|
404
|
+
*/
|
|
405
|
+
function formatCompletenessReport(scores) {
|
|
406
|
+
const lines = [];
|
|
407
|
+
lines.push('# Completeness Report');
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push(`Generated at: ${new Date().toISOString()}`);
|
|
410
|
+
lines.push('');
|
|
411
|
+
lines.push('Signal legend:');
|
|
412
|
+
lines.push('- **T** = `@cap-*` tag in source code references the AC');
|
|
413
|
+
lines.push('- **S** = a test file carries a `@cap-*` tag for the AC');
|
|
414
|
+
lines.push('- **I** = at least one test file statically imports the primary implementation');
|
|
415
|
+
lines.push('- **R** = primary file is reachable from public surface (`bin/install.js`, `hooks/*.js`)');
|
|
416
|
+
lines.push('');
|
|
417
|
+
|
|
418
|
+
const scoreboard = scores.map((s) => {
|
|
419
|
+
const avg = Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—';
|
|
420
|
+
return `| ${s.featureId} | ${s.state || '?'} | ${s.acCount} | ${avg} |`;
|
|
421
|
+
});
|
|
422
|
+
lines.push('## Summary');
|
|
423
|
+
lines.push('');
|
|
424
|
+
lines.push('| Feature | State | ACs | Avg Score |');
|
|
425
|
+
lines.push('|---------|-------|-----|-----------|');
|
|
426
|
+
lines.push(...scoreboard);
|
|
427
|
+
lines.push('');
|
|
428
|
+
|
|
429
|
+
for (const s of scores) {
|
|
430
|
+
lines.push(`## ${s.featureId}`);
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push(`State: ${s.state || '?'} — Avg: ${Number.isFinite(s.averageScore) ? s.averageScore.toFixed(2) : '—'}/4`);
|
|
433
|
+
lines.push('');
|
|
434
|
+
lines.push('| AC | T | S | I | R | Score | Reasons |');
|
|
435
|
+
lines.push('|----|---|---|---|---|-------|---------|');
|
|
436
|
+
for (const ac of s.acs) {
|
|
437
|
+
const mark = (b) => (b ? '✓' : '·');
|
|
438
|
+
const reasons = ac.reasons.join('; ').replace(/\|/g, '\\|');
|
|
439
|
+
lines.push(
|
|
440
|
+
`| ${ac.acRef} | ${mark(ac.signals.tag)} | ${mark(ac.signals.test)} | ${mark(ac.signals.testInvokesCode)} | ${mark(ac.signals.reachable)} | ${ac.score}/4 | ${reasons} |`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
lines.push('');
|
|
444
|
+
}
|
|
445
|
+
return lines.join('\n');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Load F-048 config from .cap/config.json with safe defaults.
|
|
450
|
+
* @param {string} cwd
|
|
451
|
+
* @returns {CompletenessConfig}
|
|
452
|
+
*/
|
|
453
|
+
function loadCompletenessConfig(cwd) {
|
|
454
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
455
|
+
const cfg = { ...DEFAULT_CONFIG };
|
|
456
|
+
let raw;
|
|
457
|
+
try {
|
|
458
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
459
|
+
} catch (err) {
|
|
460
|
+
// ENOENT is normal (no config yet) — stay silent. Other fs errors propagate silently too
|
|
461
|
+
// because completeness is opt-in and defaults are safe.
|
|
462
|
+
return cfg;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const parsed = JSON.parse(raw);
|
|
466
|
+
const section = parsed && parsed.completenessScore;
|
|
467
|
+
if (section && typeof section === 'object') {
|
|
468
|
+
if (typeof section.enabled === 'boolean') cfg.enabled = section.enabled;
|
|
469
|
+
if (typeof section.shipThreshold === 'number' && Number.isFinite(section.shipThreshold)) {
|
|
470
|
+
cfg.shipThreshold = section.shipThreshold;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
474
|
+
// File exists but is malformed JSON — warn once so the user sees that their
|
|
475
|
+
// opt-in settings are silently ignored. A hook env var suppresses it for CI.
|
|
476
|
+
if (!process.env.CAP_SILENT_CONFIG_WARNINGS) {
|
|
477
|
+
// eslint-disable-next-line no-console
|
|
478
|
+
console.warn(`[cap-completeness] .cap/config.json is not valid JSON (${err.message}); using defaults.`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return cfg;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Gate for `updateFeatureState(..., 'shipped')`. Returns { allowed, reason }.
|
|
486
|
+
* Only enforces when config.enabled is true. When disabled, always allows.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} featureId
|
|
489
|
+
* @param {string} targetState
|
|
490
|
+
* @param {string} cwd
|
|
491
|
+
* @param {CompletenessContext} [ctx] - Optional pre-built context (for tests / perf)
|
|
492
|
+
* @returns {{ allowed: boolean, reason: string|null, score: number|null }}
|
|
493
|
+
*/
|
|
494
|
+
function checkShipGate(featureId, targetState, cwd, ctx) {
|
|
495
|
+
if (targetState !== 'shipped') return { allowed: true, reason: null, score: null };
|
|
496
|
+
const cfg = loadCompletenessConfig(cwd);
|
|
497
|
+
if (!cfg.enabled) return { allowed: true, reason: null, score: null };
|
|
498
|
+
|
|
499
|
+
const context = ctx || buildContext(cwd);
|
|
500
|
+
const feature = (context.featureMap.features || []).find((f) => f.id === featureId);
|
|
501
|
+
if (!feature) {
|
|
502
|
+
return { allowed: true, reason: null, score: null };
|
|
503
|
+
}
|
|
504
|
+
const score = scoreFeature(feature, context);
|
|
505
|
+
if (!Number.isFinite(score.averageScore)) {
|
|
506
|
+
// No ACs — cannot compute. Allow (treat as out-of-scope for the gate).
|
|
507
|
+
return { allowed: true, reason: null, score: null };
|
|
508
|
+
}
|
|
509
|
+
if (score.averageScore < cfg.shipThreshold) {
|
|
510
|
+
return {
|
|
511
|
+
allowed: false,
|
|
512
|
+
reason:
|
|
513
|
+
`Completeness score for ${featureId} is ${score.averageScore.toFixed(2)}/4 — ` +
|
|
514
|
+
`below the configured shipThreshold=${cfg.shipThreshold}. ` +
|
|
515
|
+
`Run /cap:completeness-report for per-AC details.`,
|
|
516
|
+
score: score.averageScore,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return { allowed: true, reason: null, score: score.averageScore };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
// constants
|
|
524
|
+
DEFAULT_CONFIG,
|
|
525
|
+
// pure helpers
|
|
526
|
+
isTestFile,
|
|
527
|
+
scoreAc,
|
|
528
|
+
scoreFeature,
|
|
529
|
+
scoreAllFeatures,
|
|
530
|
+
formatFeatureBreakdown,
|
|
531
|
+
formatCompletenessReport,
|
|
532
|
+
// reachability
|
|
533
|
+
collectPublicSurfaceFiles,
|
|
534
|
+
computePublicReachable,
|
|
535
|
+
// context
|
|
536
|
+
buildContext,
|
|
537
|
+
// config + gate
|
|
538
|
+
loadCompletenessConfig,
|
|
539
|
+
checkShipGate,
|
|
540
|
+
};
|