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,315 @@
|
|
|
1
|
+
// @cap-context F-089 sharded Feature Map — pure helpers for ID validation, filename derivation,
|
|
2
|
+
// index-line parse/serialize, and surgical index-entry patching. Zero I/O except where explicitly
|
|
3
|
+
// marked (existsSync / readdirSync probes for sharded-mode detection).
|
|
4
|
+
// @cap-decision(F-089/strategy) Helpers live in a separate module so the surface stays auditable and
|
|
5
|
+
// the core cap-feature-map.cjs can lazy-require it (mirrors F-083 monorepo split pattern).
|
|
6
|
+
// @cap-pattern(F-089/test-first) Module is pure-functions where possible — every export has a unit test
|
|
7
|
+
// in tests/cap-feature-map-shard.test.cjs.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
// @cap-feature(feature:F-089, primary:true) Sharded Feature Map — Index + Per-Feature Files
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const FEATURES_DIR = 'features';
|
|
17
|
+
const FEATURE_MAP_FILE = 'FEATURE-MAP.md';
|
|
18
|
+
const MAX_ID_LENGTH = 64;
|
|
19
|
+
|
|
20
|
+
// @cap-decision(F-089/AC-3) Three-branch union accepts:
|
|
21
|
+
// 1. F-NNN legacy numeric, 3+ digits — F-001, F-1234
|
|
22
|
+
// 2. F-LONGFORM uppercase legacy (single or compound) — F-DEPLOY, F-HUB-AUTH, F-FOO_BAR (F-081 heritage)
|
|
23
|
+
// 3. F-Deskriptiv mixed-case with REQUIRED hyphen separator — F-Hub-Spotlight-Carousel, F-App2-Feature3
|
|
24
|
+
// Each branch enforces a distinct shape so collisions like `F-deploy` (lowercase single segment)
|
|
25
|
+
// are rejected — that case must be either numeric, all-uppercase, or have an explicit segment.
|
|
26
|
+
// Rejects digit-leading suffixed forms — `F-076-suffix` matches NEITHER branch:
|
|
27
|
+
// - branch 1 is digits-only
|
|
28
|
+
// - branches 2/3 require letter-first
|
|
29
|
+
// This preserves the F-076 schema invariant proven by cap-memory-schema tests.
|
|
30
|
+
// @cap-risk(reason:regex-asymmetry) The cap-feature-map header regex (`featureHeaderRE`) and
|
|
31
|
+
// surgical-patch regex (`_surgicalSetAcStatus`'s next-header) historically used the narrower
|
|
32
|
+
// F-081 pattern. F-089 widens both — keep them in sync with this constant or the parser/patcher
|
|
33
|
+
// will silently skip mixed-case IDs.
|
|
34
|
+
const FEATURE_ID_PATTERN = /^F-(?:\d{3,}|[A-Z](?:[A-Z0-9_]*[A-Z0-9])?(?:[-_][A-Z0-9_]*[A-Z0-9])*|[A-Z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+)$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} IndexEntry
|
|
38
|
+
* @property {string} id Feature ID (e.g. "F-001" or "F-Hub-Spotlight-Carousel")
|
|
39
|
+
* @property {string} state Feature lifecycle state
|
|
40
|
+
* @property {string} title Feature title
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate a feature ID. Accepts legacy numeric, F-LONGFORM, and deskriptiv mixed-case forms.
|
|
45
|
+
* Defense-in-depth: even if regex passes, FS-traversal characters must be absent.
|
|
46
|
+
* @param {*} id
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function validateFeatureId(id) {
|
|
50
|
+
if (typeof id !== 'string') return false;
|
|
51
|
+
if (id.length === 0 || id.length > MAX_ID_LENGTH) return false;
|
|
52
|
+
if (!FEATURE_ID_PATTERN.test(id)) return false;
|
|
53
|
+
// Defense-in-depth: regex already rejects these but a future loosening must not regress.
|
|
54
|
+
if (id.includes('..') || id.includes('/') || id.includes('\\')) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the per-feature filename (basename only) from a validated ID.
|
|
60
|
+
* @param {string} id
|
|
61
|
+
* @returns {string} e.g. "F-001.md"
|
|
62
|
+
*/
|
|
63
|
+
function featureFilename(id) {
|
|
64
|
+
if (!validateFeatureId(id)) {
|
|
65
|
+
throw new Error('cap: featureFilename — invalid feature ID: ' + JSON.stringify(id));
|
|
66
|
+
}
|
|
67
|
+
return id + '.md';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the absolute path to the features/ directory for a given project root + optional appPath.
|
|
72
|
+
* @param {string} projectRoot
|
|
73
|
+
* @param {string|null|undefined} [appPath]
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function featuresDirPath(projectRoot, appPath) {
|
|
77
|
+
const baseDir = appPath ? path.join(projectRoot, appPath) : projectRoot;
|
|
78
|
+
return path.join(baseDir, FEATURES_DIR);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the absolute path to a per-feature file.
|
|
83
|
+
* @param {string} projectRoot
|
|
84
|
+
* @param {string} id
|
|
85
|
+
* @param {string|null|undefined} [appPath]
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function featureFilePath(projectRoot, id, appPath) {
|
|
89
|
+
return path.join(featuresDirPath(projectRoot, appPath), featureFilename(id));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detect whether a project is in sharded mode (features/ dir exists with at least one F-*.md file).
|
|
94
|
+
* Used by readFeatureMap for AC-7 backwards-compat fallback.
|
|
95
|
+
* @param {string} projectRoot
|
|
96
|
+
* @param {string|null|undefined} [appPath]
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isShardedMap(projectRoot, appPath) {
|
|
100
|
+
const dir = featuresDirPath(projectRoot, appPath);
|
|
101
|
+
if (!fs.existsSync(dir)) return false;
|
|
102
|
+
let stat;
|
|
103
|
+
try {
|
|
104
|
+
stat = fs.statSync(dir);
|
|
105
|
+
} catch (_e) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!stat.isDirectory()) return false;
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = fs.readdirSync(dir);
|
|
112
|
+
} catch (_e) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return entries.some(e => /^F-.+\.md$/.test(e));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// @cap-decision(F-089/AC-1) Index line format: `- <ID> | <state> | <title>`.
|
|
119
|
+
// Pipe-delimited with single-space padding. Title cannot contain `|` or newlines (validated).
|
|
120
|
+
// Markdown bullet (`-`) makes the line render as a list item if the index is opened in a viewer.
|
|
121
|
+
const INDEX_LINE_RE = /^-\s+(F-\S+)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*$/;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse a single index line. Returns null on malformed input or invalid feature ID.
|
|
125
|
+
* @param {string} line
|
|
126
|
+
* @returns {IndexEntry|null}
|
|
127
|
+
*/
|
|
128
|
+
function parseIndexLine(line) {
|
|
129
|
+
const m = INDEX_LINE_RE.exec(line);
|
|
130
|
+
if (!m) return null;
|
|
131
|
+
const id = m[1];
|
|
132
|
+
if (!validateFeatureId(id)) return null;
|
|
133
|
+
return { id, state: m[2], title: m[3].trim() };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Serialize an IndexEntry to a single line.
|
|
138
|
+
* @param {IndexEntry} entry
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
function serializeIndexEntry(entry) {
|
|
142
|
+
if (!entry || typeof entry !== 'object') {
|
|
143
|
+
throw new Error('cap: serializeIndexEntry — entry must be an object');
|
|
144
|
+
}
|
|
145
|
+
if (!validateFeatureId(entry.id)) {
|
|
146
|
+
throw new Error('cap: serializeIndexEntry — invalid feature ID: ' + JSON.stringify(entry.id));
|
|
147
|
+
}
|
|
148
|
+
if (typeof entry.state !== 'string' || entry.state.length === 0 || /\s/.test(entry.state)) {
|
|
149
|
+
throw new Error('cap: serializeIndexEntry — invalid state: ' + JSON.stringify(entry.state));
|
|
150
|
+
}
|
|
151
|
+
if (typeof entry.title !== 'string' || entry.title.includes('|') || entry.title.includes('\n')) {
|
|
152
|
+
throw new Error('cap: serializeIndexEntry — title cannot contain "|" or newlines: ' + JSON.stringify(entry.title));
|
|
153
|
+
}
|
|
154
|
+
return `- ${entry.id} | ${entry.state} | ${entry.title}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse an index file (FEATURE-MAP.md in sharded mode) into IndexEntry[].
|
|
159
|
+
* Walks the `## Features` section and collects every recognized index line.
|
|
160
|
+
* Lines outside the Features section are ignored (Legend, footer, prose).
|
|
161
|
+
* @param {string} content
|
|
162
|
+
* @returns {IndexEntry[]}
|
|
163
|
+
*/
|
|
164
|
+
function parseIndex(content) {
|
|
165
|
+
const entries = [];
|
|
166
|
+
const lines = String(content).split('\n');
|
|
167
|
+
let inFeaturesSection = false;
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
if (/^##\s+Features\s*$/i.test(line)) { inFeaturesSection = true; continue; }
|
|
170
|
+
if (/^##\s/.test(line) && inFeaturesSection) { inFeaturesSection = false; continue; }
|
|
171
|
+
if (!inFeaturesSection) continue;
|
|
172
|
+
const entry = parseIndexLine(line);
|
|
173
|
+
if (entry) entries.push(entry);
|
|
174
|
+
}
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// @cap-decision(F-089/AC-9) Surgical patch — analog F-088 pattern. We rewrite the matched index
|
|
179
|
+
// line in-place via regex substitution so the rest of the file (header, prose, Legend, footer)
|
|
180
|
+
// stays byte-identical. No re-serialization of the whole index.
|
|
181
|
+
function _escapeRegex(s) {
|
|
182
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Surgically update one index entry. Returns hit:false if the ID is not present (caller appends).
|
|
187
|
+
* Validates the new title before writing — pipe/newline rejected as a hit:false miss to force
|
|
188
|
+
* the caller into the explicit append path or surface the error.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} content
|
|
191
|
+
* @param {string} id
|
|
192
|
+
* @param {{ state?: string, title?: string }} fields
|
|
193
|
+
* @returns {{ content: string, hit: boolean }}
|
|
194
|
+
*/
|
|
195
|
+
function _updateIndexEntry(content, id, fields) {
|
|
196
|
+
if (!validateFeatureId(id)) return { content, hit: false };
|
|
197
|
+
const escapedId = _escapeRegex(id);
|
|
198
|
+
// Match: `- F-NNN | state | title` (allowing leading whitespace tolerance for hand-edited files).
|
|
199
|
+
const re = new RegExp(
|
|
200
|
+
'^(\\s*-\\s+' + escapedId + '\\s*\\|\\s*)(\\w+)(\\s*\\|\\s*)([^\\n]*?)(\\s*)$',
|
|
201
|
+
'm'
|
|
202
|
+
);
|
|
203
|
+
const m = re.exec(content);
|
|
204
|
+
if (!m) return { content, hit: false };
|
|
205
|
+
const newState = fields && typeof fields.state === 'string' ? fields.state : m[2];
|
|
206
|
+
const newTitle = fields && typeof fields.title === 'string' ? fields.title : m[4];
|
|
207
|
+
if (/\s/.test(newState) || newState.length === 0) return { content, hit: false };
|
|
208
|
+
if (newTitle.includes('|') || newTitle.includes('\n')) return { content, hit: false };
|
|
209
|
+
const replaced = content.replace(re, (_full, prefix, _state, sep, _title, trailing) => {
|
|
210
|
+
return prefix + newState + sep + newTitle + trailing;
|
|
211
|
+
});
|
|
212
|
+
return { content: replaced, hit: true };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Append a new entry into the `## Features` section of the index, keeping the section anchor
|
|
217
|
+
* intact. If the section doesn't exist (template malformed), returns hit:false.
|
|
218
|
+
* The append point is just before the next `##`-level header (or end-of-content if none).
|
|
219
|
+
*
|
|
220
|
+
* @param {string} content
|
|
221
|
+
* @param {IndexEntry} entry
|
|
222
|
+
* @returns {{ content: string, hit: boolean }}
|
|
223
|
+
*/
|
|
224
|
+
function _appendIndexEntry(content, entry) {
|
|
225
|
+
const line = serializeIndexEntry(entry); // throws on invalid input — appropriate at this boundary
|
|
226
|
+
const lines = String(content).split('\n');
|
|
227
|
+
// Find the start of the `## Features` section.
|
|
228
|
+
let featuresStart = -1;
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
if (/^##\s+Features\s*$/i.test(lines[i])) { featuresStart = i; break; }
|
|
231
|
+
}
|
|
232
|
+
if (featuresStart === -1) return { content, hit: false };
|
|
233
|
+
// Find the end of the Features section: the next `## ...` header, or end-of-file.
|
|
234
|
+
let featuresEnd = lines.length;
|
|
235
|
+
for (let i = featuresStart + 1; i < lines.length; i++) {
|
|
236
|
+
if (/^##\s/.test(lines[i])) { featuresEnd = i; break; }
|
|
237
|
+
}
|
|
238
|
+
// Insert position: just before featuresEnd, but trim trailing blanks within the section first
|
|
239
|
+
// so we don't accumulate blank lines on repeated appends.
|
|
240
|
+
let insertAt = featuresEnd;
|
|
241
|
+
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === '') insertAt--;
|
|
242
|
+
// Build new lines: keep through insertAt-1, splice in the entry line, then the rest.
|
|
243
|
+
// We do NOT add a trailing blank — `after` (the slice from insertAt onward) typically
|
|
244
|
+
// starts with the section's existing blank-line padding before the next `##` header,
|
|
245
|
+
// so we'd otherwise accumulate blank lines on every append.
|
|
246
|
+
const before = lines.slice(0, insertAt);
|
|
247
|
+
const after = lines.slice(insertAt);
|
|
248
|
+
return { content: before.concat([line], after).join('\n'), hit: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Serialize a complete index file from scratch (header + Features section + Legend + footer).
|
|
253
|
+
* Used by the migrator to build the initial index after sharding. Surgical patches handle
|
|
254
|
+
* subsequent updates so this serializer is only called on full rebuilds.
|
|
255
|
+
*
|
|
256
|
+
* @param {IndexEntry[]} entries
|
|
257
|
+
* @param {{ now?: () => Date }} [options]
|
|
258
|
+
* @returns {string}
|
|
259
|
+
*/
|
|
260
|
+
function serializeIndex(entries, options) {
|
|
261
|
+
const now = options && typeof options.now === 'function' ? options.now() : new Date();
|
|
262
|
+
const lines = [
|
|
263
|
+
'# Feature Map',
|
|
264
|
+
'',
|
|
265
|
+
'> Single source of truth — sharded layout (F-089). Each feature has its own file in `features/<ID>.md`.',
|
|
266
|
+
'> The index below lists every feature with id, state, and title; load the per-feature file for full details.',
|
|
267
|
+
'',
|
|
268
|
+
'## Features',
|
|
269
|
+
'',
|
|
270
|
+
];
|
|
271
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
272
|
+
for (const e of entries) lines.push(serializeIndexEntry(e));
|
|
273
|
+
lines.push('');
|
|
274
|
+
} else {
|
|
275
|
+
lines.push('<!-- No features yet. Run /cap:brainstorm or add features. -->');
|
|
276
|
+
lines.push('');
|
|
277
|
+
}
|
|
278
|
+
lines.push('## Legend');
|
|
279
|
+
lines.push('');
|
|
280
|
+
lines.push('| State | Meaning |');
|
|
281
|
+
lines.push('|-------|---------|');
|
|
282
|
+
lines.push('| planned | Feature identified, not yet implemented |');
|
|
283
|
+
lines.push('| prototyped | Initial implementation exists |');
|
|
284
|
+
lines.push('| tested | Tests written and passing |');
|
|
285
|
+
lines.push('| shipped | Deployed / merged to main |');
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push('---');
|
|
288
|
+
lines.push(`*Last updated: ${now.toISOString()}*`);
|
|
289
|
+
lines.push('');
|
|
290
|
+
return lines.join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
// Constants
|
|
295
|
+
FEATURES_DIR,
|
|
296
|
+
FEATURE_MAP_FILE,
|
|
297
|
+
MAX_ID_LENGTH,
|
|
298
|
+
FEATURE_ID_PATTERN,
|
|
299
|
+
// Validation + path helpers
|
|
300
|
+
validateFeatureId,
|
|
301
|
+
featureFilename,
|
|
302
|
+
featuresDirPath,
|
|
303
|
+
featureFilePath,
|
|
304
|
+
isShardedMap,
|
|
305
|
+
// Index parse/serialize
|
|
306
|
+
parseIndexLine,
|
|
307
|
+
serializeIndexEntry,
|
|
308
|
+
parseIndex,
|
|
309
|
+
serializeIndex,
|
|
310
|
+
// Surgical updates
|
|
311
|
+
_updateIndexEntry,
|
|
312
|
+
_appendIndexEntry,
|
|
313
|
+
// Internal (exposed for tests)
|
|
314
|
+
_escapeRegex,
|
|
315
|
+
};
|