peaks-cli 1.4.2 → 2.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/marketplace.json +51 -0
- package/CHANGELOG.md +238 -0
- package/README-en.md +226 -0
- package/README.md +152 -122
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +60 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +86 -0
- package/dist/src/cli/program.js +30 -0
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +92 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +35 -2
- package/dist/src/services/config/config-service.js +81 -0
- package/dist/src/services/config/config-types.d.ts +58 -0
- package/dist/src/services/config/config-types.js +6 -0
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +43 -0
- package/dist/src/services/skills/sync-service.js +99 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +8 -2
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +10 -4
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* red-lines-service — main entry. Orchestrates the three tree scanners,
|
|
3
|
+
* the classifier, and the backing detector, then assembles the final
|
|
4
|
+
* RedLineAudit envelope.
|
|
5
|
+
*
|
|
6
|
+
* Pipeline (per openspec/changes/2026-06-11-l2-1-redlines-audit/design.md):
|
|
7
|
+
* 1. Run all 3 scanners in parallel (skills, rules, openspec)
|
|
8
|
+
* 2. Classifier turns MarkdownLine[] into RedLineEntry[]
|
|
9
|
+
* 3. Backing detector re-classifies each entry (cli-backed vs partial vs prose-only)
|
|
10
|
+
* 4. Tally + return RedLineAudit
|
|
11
|
+
*
|
|
12
|
+
* Sub-agent-sid enforcer (Task 2) is also invoked here: it dogfoods Slice 0.5
|
|
13
|
+
* sid-naming-guard and adds any invalid sids as warnings.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { classifyFiles } from './classifier.js';
|
|
18
|
+
import { classifyBackingBatch } from './backing-detector.js';
|
|
19
|
+
import { scanSkillsTree } from './scanners/skills-tree-scanner.js';
|
|
20
|
+
import { scanRulesTree } from './scanners/rules-tree-scanner.js';
|
|
21
|
+
import { scanOpenSpecTree } from './scanners/openspec-scanner.js';
|
|
22
|
+
import { findInvalidSubAgentSids, findInvalidRuntimeSids } from './enforcers/sub-agent-sid.js';
|
|
23
|
+
import { checkTechDocPresence } from './enforcers/tech-doc-presence.js';
|
|
24
|
+
import { findStubMarkers } from './enforcers/prototype-fidelity.js';
|
|
25
|
+
import { checkDesignDraftConfirmation } from './enforcers/design-draft-confirm.js';
|
|
26
|
+
import { checkPreRdScan } from './enforcers/pre-rd-scan.js';
|
|
27
|
+
import { readSkillFiles, lintSectionShape, lintSectionOrder, lintFrontmatterShape, lintReferenceLoadStrategy, } from './enforcers/lint-style.js';
|
|
28
|
+
import { lintRefPathResolves, lintNoBrokenMkdir, lintNoPwdSymlinkJumps, lintNoRelativeArchivePaths, } from './enforcers/lint-reference-integrity.js';
|
|
29
|
+
import { lintCliBackMandatorText, lintCliBackNoOrphanBlocking, lintCliBackNoOrphanMustNot, } from './enforcers/lint-cli-back.js';
|
|
30
|
+
import { lintNoFluff, lintNoClosingPrompt, lintStatusHeader, } from './enforcers/lint-output-style.js';
|
|
31
|
+
import { lintOpenSpecAcceptanceBullets, lintOpenSpecSpecReference, lintTechDocPresenceShape, lintPeaksDoctorAcknowledged, } from './enforcers/lint-workflow-shape.js';
|
|
32
|
+
import { lintCatalogSize, lintCatalogProseOnlyRatio, } from './enforcers/lint-catalog-governance.js';
|
|
33
|
+
import { lintH1TitleRequired, lintApplicableTaskLevels, lintSeeAlsoSection, lintCrossRefResolves, lintNoSelfReference, lintNoOrphanLink, lintLineCountLe800, lintH2CountLe12, lintOverviewNearTop, lintLoadStrategyOnDemandFallback, lintLoadStrategyAlwaysCacheable, lintNoBashHeredoc, lintNoSudo, lintNoCurlPipeBash, lintCodeBlockLanguage, lintNoFakePrompt, lintNoAbsolutePaths, lintNoChmod777, lintNoMagicNumbers, lintSkillCitesEveryReference, lintLoadStrategyMatchesSize, readReferenceFiles, } from './enforcers/lint-reference-shape.js';
|
|
34
|
+
import { lintCatalogStability, lintNoOrphanEnforcer, lintNoOrphanCatalog, lintRuntimeBudget, readCatalogHistory, } from './enforcers/lint-audit-regression.js';
|
|
35
|
+
function buildFileInputs(skills, rules, openspec) {
|
|
36
|
+
const grouped = new Map();
|
|
37
|
+
for (const line of [...skills.lines, ...rules.lines, ...openspec.lines]) {
|
|
38
|
+
const existing = grouped.get(line.file);
|
|
39
|
+
if (existing) {
|
|
40
|
+
// line numbers are 1-based; pad to ensure the right slot
|
|
41
|
+
while (existing.length < line.line)
|
|
42
|
+
existing.push('');
|
|
43
|
+
existing[line.line - 1] = line.text;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const arr = [];
|
|
47
|
+
while (arr.length < line.line - 1)
|
|
48
|
+
arr.push('');
|
|
49
|
+
arr.push(line.text);
|
|
50
|
+
grouped.set(line.file, arr);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Array.from(grouped.entries()).map(([file, lines]) => ({ file, lines }));
|
|
54
|
+
}
|
|
55
|
+
function tally(entries) {
|
|
56
|
+
let cliBacked = 0;
|
|
57
|
+
let partial = 0;
|
|
58
|
+
let proseOnly = 0;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.backing === 'cli-backed')
|
|
61
|
+
cliBacked++;
|
|
62
|
+
else if (entry.backing === 'partial')
|
|
63
|
+
partial++;
|
|
64
|
+
else
|
|
65
|
+
proseOnly++;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
totalRedLines: entries.length,
|
|
69
|
+
cliBacked,
|
|
70
|
+
partial,
|
|
71
|
+
proseOnly,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function runRedLinesAudit(input) {
|
|
75
|
+
// Capture the audit start time. The P2-b runtime-budget enforcer
|
|
76
|
+
// uses this to assert that peaks audit red-lines completes in
|
|
77
|
+
// < 2 seconds on a 100-reference project.
|
|
78
|
+
const auditStartMs = Date.now();
|
|
79
|
+
const skills = scanSkillsTree({ projectRoot: input.projectRoot });
|
|
80
|
+
const rules = scanRulesTree({ projectRoot: input.projectRoot });
|
|
81
|
+
const openspec = scanOpenSpecTree({ projectRoot: input.projectRoot });
|
|
82
|
+
const fileInputs = buildFileInputs(skills, rules, openspec);
|
|
83
|
+
const classified = classifyFiles(fileInputs);
|
|
84
|
+
const backed = classifyBackingBatch(classified.entries, input.projectRoot);
|
|
85
|
+
// Sub-agent-sid enforcer (Task 2): dogfoods Slice 0.5 sid-naming-guard.
|
|
86
|
+
const subAgentSids = findInvalidSubAgentSids(input.projectRoot);
|
|
87
|
+
const runtimeSids = findInvalidRuntimeSids(input.projectRoot);
|
|
88
|
+
const warnings = [
|
|
89
|
+
...skills.warnings,
|
|
90
|
+
...rules.warnings,
|
|
91
|
+
...openspec.warnings,
|
|
92
|
+
...classified.warnings.map((message) => ({ file: '(classifier)', message })),
|
|
93
|
+
...backed.warnings.map((message) => ({ file: '(backing-detector)', message })),
|
|
94
|
+
];
|
|
95
|
+
if (subAgentSids.scanned && subAgentSids.invalid.length > 0) {
|
|
96
|
+
for (const sid of subAgentSids.invalid) {
|
|
97
|
+
warnings.push({
|
|
98
|
+
file: '.peaks/_sub_agents/' + sid,
|
|
99
|
+
message: `invalid sub-agent sid: "${sid}" (does not match isValidSessionId)`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (runtimeSids.scanned && runtimeSids.invalid.length > 0) {
|
|
104
|
+
for (const sid of runtimeSids.invalid) {
|
|
105
|
+
warnings.push({
|
|
106
|
+
file: '.peaks/_runtime/' + sid,
|
|
107
|
+
message: `invalid runtime sid: "${sid}" (does not match isValidSessionId)`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const counts = tally(backed.entries);
|
|
112
|
+
// L2.4 P2-b: invoke the 5 P0/P1 file-system enforcers during the scan.
|
|
113
|
+
// Each enforcer function returns a structured result; we convert to
|
|
114
|
+
// EnforcerFinding[] and add to the audit output. The audit scanner
|
|
115
|
+
// actually CALLS the enforcers, not just catalogs them.
|
|
116
|
+
const enforcerFindings = [];
|
|
117
|
+
// 1. sub-agent-sid (already partially handled via warnings; here we
|
|
118
|
+
// add a structured finding for the same data).
|
|
119
|
+
if (subAgentSids.scanned && subAgentSids.invalid.length > 0) {
|
|
120
|
+
for (const sid of subAgentSids.invalid) {
|
|
121
|
+
enforcerFindings.push({
|
|
122
|
+
enforcerId: 'rl-sub-agent-sid-001',
|
|
123
|
+
rule: 'Sub-Agent SID Isolation',
|
|
124
|
+
severity: 'fail',
|
|
125
|
+
file: `.peaks/_sub_agents/${sid}`,
|
|
126
|
+
detail: `invalid sub-agent sid: "${sid}" (does not match isValidSessionId)`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (runtimeSids.scanned && runtimeSids.invalid.length > 0) {
|
|
131
|
+
for (const sid of runtimeSids.invalid) {
|
|
132
|
+
enforcerFindings.push({
|
|
133
|
+
enforcerId: 'rl-sub-agent-sid-001',
|
|
134
|
+
rule: 'Sub-Agent SID Isolation',
|
|
135
|
+
severity: 'fail',
|
|
136
|
+
file: `.peaks/_runtime/${sid}`,
|
|
137
|
+
detail: `invalid runtime sid: "${sid}" (does not match isValidSessionId)`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// 2. tech-doc-presence — check the current session's tech-doc.md.
|
|
142
|
+
// The sessionId comes from the canonical session binding file
|
|
143
|
+
// (.peaks/_runtime/session.json) when present.
|
|
144
|
+
const sessionJsonPath = `${input.projectRoot}/.peaks/_runtime/session.json`;
|
|
145
|
+
if (existsSync(sessionJsonPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const sessionData = JSON.parse(require('node:fs').readFileSync(sessionJsonPath, 'utf8'));
|
|
148
|
+
if (typeof sessionData.peakSessionId === 'string' && sessionData.peakSessionId.length > 0) {
|
|
149
|
+
const techDoc = checkTechDocPresence({ projectRoot: input.projectRoot, sessionId: sessionData.peakSessionId });
|
|
150
|
+
if (!techDoc.exists) {
|
|
151
|
+
enforcerFindings.push({
|
|
152
|
+
enforcerId: 'rl-tech-doc-presence-001',
|
|
153
|
+
rule: 'Tech-Doc Presence',
|
|
154
|
+
severity: 'fail',
|
|
155
|
+
file: techDoc.path,
|
|
156
|
+
detail: 'tech-doc.md missing (rd → spec-locked transition will refuse)',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (techDoc.isEmpty) {
|
|
160
|
+
enforcerFindings.push({
|
|
161
|
+
enforcerId: 'rl-tech-doc-presence-001',
|
|
162
|
+
rule: 'Tech-Doc Presence',
|
|
163
|
+
severity: 'fail',
|
|
164
|
+
file: techDoc.path,
|
|
165
|
+
detail: 'tech-doc.md is 0 bytes',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// skip malformed session.json
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 3. pre-rd-scan — check whether project-scan.md and standards-preflight.json exist
|
|
175
|
+
// for the current session.
|
|
176
|
+
if (existsSync(sessionJsonPath)) {
|
|
177
|
+
try {
|
|
178
|
+
const sessionData = JSON.parse(require('node:fs').readFileSync(sessionJsonPath, 'utf8'));
|
|
179
|
+
if (typeof sessionData.peakSessionId === 'string' && sessionData.peakSessionId.length > 0) {
|
|
180
|
+
const preRd = checkPreRdScan({ projectRoot: input.projectRoot, sessionId: sessionData.peakSessionId });
|
|
181
|
+
if (!preRd.archetypeScanned) {
|
|
182
|
+
enforcerFindings.push({
|
|
183
|
+
enforcerId: 'rl-pre-rd-scan-001',
|
|
184
|
+
rule: 'Pre-RD Scan: Archetype Detected',
|
|
185
|
+
severity: 'warn',
|
|
186
|
+
file: preRd.archetypeReportPath,
|
|
187
|
+
detail: 'project-scan.md not produced; rd work has no archetype context',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!preRd.standardsPreflightDone) {
|
|
191
|
+
enforcerFindings.push({
|
|
192
|
+
enforcerId: 'rl-pre-rd-scan-002',
|
|
193
|
+
rule: 'Pre-RD Scan: Standards Preflight',
|
|
194
|
+
severity: 'warn',
|
|
195
|
+
file: preRd.standardsReportPath,
|
|
196
|
+
detail: 'standards-preflight.json not produced; rd work has no project standards context',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// skip
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 4. design-draft-confirm — check the current change-id's design-draft.md.
|
|
206
|
+
// The change-id is the .peaks/<changeId>/ui/design-draft.md path.
|
|
207
|
+
// For the audit, we look for any .peaks/*/ui/design-draft.md.
|
|
208
|
+
const peaksDir = `${input.projectRoot}/.peaks`;
|
|
209
|
+
if (existsSync(peaksDir)) {
|
|
210
|
+
try {
|
|
211
|
+
const entries = require('node:fs').readdirSync(peaksDir);
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry === '_archive' || entry === '_runtime' || entry === '_sub_agents' || entry.startsWith('.'))
|
|
214
|
+
continue;
|
|
215
|
+
const designCheck = checkDesignDraftConfirmation({
|
|
216
|
+
projectRoot: input.projectRoot,
|
|
217
|
+
sessionId: '',
|
|
218
|
+
changeId: entry,
|
|
219
|
+
});
|
|
220
|
+
if (designCheck.draftExists && !designCheck.confirmed) {
|
|
221
|
+
enforcerFindings.push({
|
|
222
|
+
enforcerId: 'rl-design-draft-confirm-002',
|
|
223
|
+
rule: 'Design-Draft Confirm: Confirmed State',
|
|
224
|
+
severity: 'warn',
|
|
225
|
+
file: designCheck.draftPath,
|
|
226
|
+
detail: 'design-draft.md exists but is not confirmed (no "confirmed" marker)',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// skip
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// 5. prototype-fidelity — scan recent src/ files for stub markers
|
|
236
|
+
// (TODO/FIXME/XXX). Limit to 50 most-recently-modified files
|
|
237
|
+
// to keep scan fast.
|
|
238
|
+
try {
|
|
239
|
+
const srcDir = `${input.projectRoot}/src`;
|
|
240
|
+
if (existsSync(srcDir)) {
|
|
241
|
+
const allFiles = [];
|
|
242
|
+
const walk = (dir) => {
|
|
243
|
+
const ents = require('node:fs').readdirSync(dir, { withFileTypes: true });
|
|
244
|
+
for (const e of ents) {
|
|
245
|
+
const full = `${dir}/${e.name}`;
|
|
246
|
+
if (e.isDirectory()) {
|
|
247
|
+
if (e.name === 'node_modules' || e.name === 'dist')
|
|
248
|
+
continue;
|
|
249
|
+
walk(full);
|
|
250
|
+
}
|
|
251
|
+
else if (e.isFile() && /\.(ts|tsx|js|mjs)$/.test(e.name)) {
|
|
252
|
+
const rel = full.slice(input.projectRoot.length + 1).split('\\').join('/');
|
|
253
|
+
allFiles.push(rel);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
walk(srcDir);
|
|
258
|
+
const sample = allFiles.slice(0, 50);
|
|
259
|
+
const stubHits = findStubMarkers({ projectRoot: input.projectRoot, filePaths: sample });
|
|
260
|
+
for (const hit of stubHits.stubMarkers.slice(0, 10)) {
|
|
261
|
+
enforcerFindings.push({
|
|
262
|
+
enforcerId: 'rl-prototype-fidelity-001',
|
|
263
|
+
rule: 'Prototype Fidelity: No Stub Markers',
|
|
264
|
+
severity: 'warn',
|
|
265
|
+
file: hit.filePath,
|
|
266
|
+
detail: `stub marker "${hit.pattern}" at line containing: ${hit.snippet.slice(0, 50)}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// skip
|
|
273
|
+
}
|
|
274
|
+
// 6. P2-a enforcers (Slice #6 L2.3) — 18 lint-style enforcers
|
|
275
|
+
// across Themes A (section), B (frontmatter), C (output
|
|
276
|
+
// style), D (CLI-back gaps), E (reference integrity),
|
|
277
|
+
// F (workflow shape), G (catalog governance). Each enforcer
|
|
278
|
+
// is a pure pattern scan; we walk `skills/` + `references/`,
|
|
279
|
+
// call the helpers, and convert LintHit[] into
|
|
280
|
+
// EnforcerFinding[]. Failures here are WARN, not FAIL —
|
|
281
|
+
// P2-a is the lint layer, not the structural gate layer.
|
|
282
|
+
try {
|
|
283
|
+
const skillsRoot = join(input.projectRoot, 'skills');
|
|
284
|
+
if (existsSync(skillsRoot)) {
|
|
285
|
+
const skillNames = [];
|
|
286
|
+
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
287
|
+
if (!entry.isDirectory())
|
|
288
|
+
continue;
|
|
289
|
+
if (entry.name.startsWith('.'))
|
|
290
|
+
continue;
|
|
291
|
+
if (!existsSync(join(skillsRoot, entry.name, 'SKILL.md')))
|
|
292
|
+
continue;
|
|
293
|
+
skillNames.push(entry.name);
|
|
294
|
+
}
|
|
295
|
+
const skillFiles = readSkillFiles(skillsRoot, skillNames);
|
|
296
|
+
for (const skill of skillFiles) {
|
|
297
|
+
const refsDir = join(skillsRoot, skill.name, 'references');
|
|
298
|
+
const refs = existsSync(refsDir)
|
|
299
|
+
? readdirSync(refsDir).filter((f) => f.endsWith('.md'))
|
|
300
|
+
: [];
|
|
301
|
+
const lintHits = [
|
|
302
|
+
...lintSectionShape(skill),
|
|
303
|
+
...lintSectionOrder(skill),
|
|
304
|
+
...lintFrontmatterShape(skill),
|
|
305
|
+
...lintRefPathResolves(skillsRoot, skill.name, refs),
|
|
306
|
+
...lintNoBrokenMkdir(skill),
|
|
307
|
+
...lintNoPwdSymlinkJumps(skill),
|
|
308
|
+
...lintNoRelativeArchivePaths(skill),
|
|
309
|
+
...lintReferenceLoadStrategy(refsDir, refs),
|
|
310
|
+
...lintCliBackMandatorText(skill),
|
|
311
|
+
...lintCliBackNoOrphanBlocking(skill),
|
|
312
|
+
...lintCliBackNoOrphanMustNot(skill),
|
|
313
|
+
...lintNoFluff(skill),
|
|
314
|
+
...lintNoClosingPrompt(skill),
|
|
315
|
+
...lintPeaksDoctorAcknowledged(skill),
|
|
316
|
+
];
|
|
317
|
+
for (const hit of lintHits) {
|
|
318
|
+
enforcerFindings.push({
|
|
319
|
+
enforcerId: hit.catalogId,
|
|
320
|
+
rule: hit.rule,
|
|
321
|
+
severity: 'warn',
|
|
322
|
+
file: hit.file,
|
|
323
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// 7. P2-b enforcers (Slice #7 L2.4) — references/*.md
|
|
327
|
+
// shape enforcers (Themes H-K, M-P). Each enforcer
|
|
328
|
+
// walks the reference files of this skill and reports
|
|
329
|
+
// per-file hits.
|
|
330
|
+
try {
|
|
331
|
+
const refFiles = readReferenceFiles(skillsRoot, skill.name, refs);
|
|
332
|
+
for (const ref of refFiles) {
|
|
333
|
+
const refHits = [
|
|
334
|
+
...lintH1TitleRequired(ref),
|
|
335
|
+
...lintApplicableTaskLevels(ref),
|
|
336
|
+
...lintSeeAlsoSection(ref),
|
|
337
|
+
...lintCrossRefResolves(ref, refsDir, refs),
|
|
338
|
+
...lintNoSelfReference(ref),
|
|
339
|
+
...lintNoOrphanLink(ref),
|
|
340
|
+
...lintLineCountLe800(ref),
|
|
341
|
+
...lintH2CountLe12(ref),
|
|
342
|
+
...lintOverviewNearTop(ref),
|
|
343
|
+
...lintLoadStrategyOnDemandFallback(ref),
|
|
344
|
+
...lintLoadStrategyAlwaysCacheable(ref),
|
|
345
|
+
...lintNoBashHeredoc(ref),
|
|
346
|
+
...lintNoSudo(ref),
|
|
347
|
+
...lintNoCurlPipeBash(ref),
|
|
348
|
+
...lintCodeBlockLanguage(ref),
|
|
349
|
+
...lintNoFakePrompt(ref),
|
|
350
|
+
...lintNoAbsolutePaths(ref),
|
|
351
|
+
...lintNoChmod777(ref),
|
|
352
|
+
...lintNoMagicNumbers(ref),
|
|
353
|
+
...lintSkillCitesEveryReference(ref, skill),
|
|
354
|
+
...lintLoadStrategyMatchesSize(ref),
|
|
355
|
+
];
|
|
356
|
+
for (const hit of refHits) {
|
|
357
|
+
enforcerFindings.push({
|
|
358
|
+
enforcerId: hit.catalogId,
|
|
359
|
+
rule: hit.rule,
|
|
360
|
+
severity: 'warn',
|
|
361
|
+
file: hit.file,
|
|
362
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// skip — P2-b enforcers are best-effort per reference file
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// skip — P2-a enforcers are best-effort; a failure here must
|
|
375
|
+
// not break the audit pipeline
|
|
376
|
+
}
|
|
377
|
+
// 8. P2-b Theme L — audit regression enforcers. These check
|
|
378
|
+
// the audit framework's own integrity (catalog stability,
|
|
379
|
+
// no orphan enforcers, no orphan catalog entries, runtime
|
|
380
|
+
// budget). They are the gating layer that `peaks slice check`
|
|
381
|
+
// asserts in its 5th stage.
|
|
382
|
+
try {
|
|
383
|
+
const catalogSize = backed.entries.length;
|
|
384
|
+
const proseOnlyCount = counts.proseOnly;
|
|
385
|
+
const observedMs = Date.now() - auditStartMs;
|
|
386
|
+
const auditRegressionHits = [
|
|
387
|
+
...lintCatalogStability({
|
|
388
|
+
currentSize: catalogSize,
|
|
389
|
+
sizeNinetyDaysAgo: readCatalogHistory(input.projectRoot),
|
|
390
|
+
}),
|
|
391
|
+
...lintNoOrphanEnforcer(input.projectRoot),
|
|
392
|
+
...lintNoOrphanCatalog(),
|
|
393
|
+
...lintRuntimeBudget(input.projectRoot, observedMs),
|
|
394
|
+
];
|
|
395
|
+
for (const hit of auditRegressionHits) {
|
|
396
|
+
enforcerFindings.push({
|
|
397
|
+
enforcerId: hit.catalogId,
|
|
398
|
+
rule: hit.rule,
|
|
399
|
+
severity: 'warn',
|
|
400
|
+
file: hit.file,
|
|
401
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// skip — audit-regression enforcers are best-effort
|
|
407
|
+
}
|
|
408
|
+
// P2-a Theme F (project-level): openspec proposals + tech-doc shape.
|
|
409
|
+
try {
|
|
410
|
+
const openspecHits = [
|
|
411
|
+
...lintOpenSpecAcceptanceBullets(input.projectRoot),
|
|
412
|
+
...lintOpenSpecSpecReference(input.projectRoot),
|
|
413
|
+
...lintTechDocPresenceShape(input.projectRoot),
|
|
414
|
+
];
|
|
415
|
+
for (const hit of openspecHits) {
|
|
416
|
+
enforcerFindings.push({
|
|
417
|
+
enforcerId: hit.catalogId,
|
|
418
|
+
rule: hit.rule,
|
|
419
|
+
severity: 'warn',
|
|
420
|
+
file: hit.file,
|
|
421
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// skip
|
|
427
|
+
}
|
|
428
|
+
// P2-a Theme C (session log): status header presence. Reads
|
|
429
|
+
// .peaks/_runtime/<sid>/session.log. Soft check: if no session
|
|
430
|
+
// log is present (e.g. dogfood on a fresh project), the enforcer
|
|
431
|
+
// returns an empty array.
|
|
432
|
+
try {
|
|
433
|
+
let peakSessionId = '';
|
|
434
|
+
if (existsSync(sessionJsonPath)) {
|
|
435
|
+
const sessionData = JSON.parse(readFileSync(sessionJsonPath, 'utf8'));
|
|
436
|
+
if (typeof sessionData.peakSessionId === 'string') {
|
|
437
|
+
peakSessionId = sessionData.peakSessionId;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (peakSessionId.length > 0) {
|
|
441
|
+
const statusHits = lintStatusHeader(input.projectRoot, peakSessionId);
|
|
442
|
+
for (const hit of statusHits) {
|
|
443
|
+
enforcerFindings.push({
|
|
444
|
+
enforcerId: hit.catalogId,
|
|
445
|
+
rule: hit.rule,
|
|
446
|
+
severity: 'warn',
|
|
447
|
+
file: hit.file,
|
|
448
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// skip
|
|
455
|
+
}
|
|
456
|
+
// P2-a Theme G: catalog governance. Two checks: catalog size and
|
|
457
|
+
// prose-only ratio. Both are static checks on the catalog itself,
|
|
458
|
+
// not on the project.
|
|
459
|
+
try {
|
|
460
|
+
const catalogSize = backed.entries.length;
|
|
461
|
+
const proseOnlyCount = counts.proseOnly;
|
|
462
|
+
const catalogSizeHits = lintCatalogSize(catalogSize);
|
|
463
|
+
const ratioHits = lintCatalogProseOnlyRatio(catalogSize, proseOnlyCount);
|
|
464
|
+
for (const hit of [...catalogSizeHits, ...ratioHits]) {
|
|
465
|
+
enforcerFindings.push({
|
|
466
|
+
enforcerId: hit.catalogId,
|
|
467
|
+
rule: hit.rule,
|
|
468
|
+
severity: 'warn',
|
|
469
|
+
file: hit.file,
|
|
470
|
+
detail: `line ${hit.line}: ${hit.matchedText}`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// skip
|
|
476
|
+
}
|
|
477
|
+
const audit = {
|
|
478
|
+
totalRedLines: counts.totalRedLines,
|
|
479
|
+
cliBacked: counts.cliBacked,
|
|
480
|
+
partial: counts.partial,
|
|
481
|
+
proseOnly: counts.proseOnly,
|
|
482
|
+
audit: backed.entries,
|
|
483
|
+
enforcerFindings,
|
|
484
|
+
};
|
|
485
|
+
return { audit, warnings };
|
|
486
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openspec-scanner — walks every markdown file under `openspec/changes/`
|
|
3
|
+
* (recursive) and returns each file's raw lines. OpenSpec change records
|
|
4
|
+
* often contain red lines for the slice they describe; the audit framework
|
|
5
|
+
* picks them up.
|
|
6
|
+
*/
|
|
7
|
+
import type { MarkdownLine, ScanWarning } from '../types.js';
|
|
8
|
+
export interface OpenSpecScanInput {
|
|
9
|
+
readonly projectRoot: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenSpecScanResult {
|
|
12
|
+
readonly lines: readonly MarkdownLine[];
|
|
13
|
+
readonly warnings: readonly ScanWarning[];
|
|
14
|
+
}
|
|
15
|
+
export declare function scanOpenSpecTree(input: OpenSpecScanInput): OpenSpecScanResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openspec-scanner — walks every markdown file under `openspec/changes/`
|
|
3
|
+
* (recursive) and returns each file's raw lines. OpenSpec change records
|
|
4
|
+
* often contain red lines for the slice they describe; the audit framework
|
|
5
|
+
* picks them up.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
const OPENSPEC_DIR = 'openspec/changes';
|
|
10
|
+
function walkOpenSpecDir(projectRoot, dir, out) {
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
out.warnings.push({
|
|
17
|
+
file: relative(projectRoot, dir).split('\\').join('/'),
|
|
18
|
+
message: `readdir failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const full = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
walkOpenSpecDir(projectRoot, full, out);
|
|
26
|
+
}
|
|
27
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
content = readFileSync(full, 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
out.warnings.push({
|
|
34
|
+
file: relative(projectRoot, full).split('\\').join('/'),
|
|
35
|
+
message: `read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const rel = relative(projectRoot, full).split('\\').join('/');
|
|
40
|
+
const lines = content.split(/\r?\n/);
|
|
41
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
42
|
+
out.lines.push({ file: rel, line: idx + 1, text: lines[idx] ?? '' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function scanOpenSpecTree(input) {
|
|
48
|
+
const root = join(input.projectRoot, OPENSPEC_DIR);
|
|
49
|
+
if (!existsSync(root)) {
|
|
50
|
+
return { lines: [], warnings: [] };
|
|
51
|
+
}
|
|
52
|
+
const out = { lines: [], warnings: [] };
|
|
53
|
+
walkOpenSpecDir(input.projectRoot, root, out);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rules-tree-scanner — walks every markdown file under `.claude/rules/`
|
|
3
|
+
* (recursive) and returns each file's raw lines for the classifier.
|
|
4
|
+
*
|
|
5
|
+
* Per `static-scan-must-cover-skills-tree-not-just-src.md` and the L2
|
|
6
|
+
* redesign §5.2, the audit must cover .claude/rules in addition to skills.
|
|
7
|
+
*/
|
|
8
|
+
import type { MarkdownLine, ScanWarning } from '../types.js';
|
|
9
|
+
export interface RulesTreeScanInput {
|
|
10
|
+
readonly projectRoot: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RulesTreeScanResult {
|
|
13
|
+
readonly lines: readonly MarkdownLine[];
|
|
14
|
+
readonly warnings: readonly ScanWarning[];
|
|
15
|
+
}
|
|
16
|
+
export declare function scanRulesTree(input: RulesTreeScanInput): RulesTreeScanResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rules-tree-scanner — walks every markdown file under `.claude/rules/`
|
|
3
|
+
* (recursive) and returns each file's raw lines for the classifier.
|
|
4
|
+
*
|
|
5
|
+
* Per `static-scan-must-cover-skills-tree-not-just-src.md` and the L2
|
|
6
|
+
* redesign §5.2, the audit must cover .claude/rules in addition to skills.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join, relative } from 'node:path';
|
|
10
|
+
const RULES_DIR = '.claude/rules';
|
|
11
|
+
function walkRulesDir(projectRoot, dir, out) {
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
out.warnings.push({
|
|
18
|
+
file: relative(projectRoot, dir).split('\\').join('/'),
|
|
19
|
+
message: `readdir failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const full = join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
walkRulesDir(projectRoot, full, out);
|
|
27
|
+
}
|
|
28
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
29
|
+
let content;
|
|
30
|
+
try {
|
|
31
|
+
content = readFileSync(full, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
out.warnings.push({
|
|
35
|
+
file: relative(projectRoot, full).split('\\').join('/'),
|
|
36
|
+
message: `read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const rel = relative(projectRoot, full).split('\\').join('/');
|
|
41
|
+
const lines = content.split(/\r?\n/);
|
|
42
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
43
|
+
out.lines.push({ file: rel, line: idx + 1, text: lines[idx] ?? '' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function scanRulesTree(input) {
|
|
49
|
+
const rulesRoot = join(input.projectRoot, RULES_DIR);
|
|
50
|
+
if (!existsSync(rulesRoot)) {
|
|
51
|
+
return { lines: [], warnings: [] };
|
|
52
|
+
}
|
|
53
|
+
const out = { lines: [], warnings: [] };
|
|
54
|
+
walkRulesDir(input.projectRoot, rulesRoot, out);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skills-tree-scanner — walks each `skills/<name>/SKILL.md` and returns
|
|
3
|
+
* each file's raw lines for the classifier to consume.
|
|
4
|
+
*
|
|
5
|
+
* Per `static-scan-must-cover-skills-tree-not-just-src.md`, the red-line
|
|
6
|
+
* audit MUST cover the skills tree, not just `src/`. This scanner is the
|
|
7
|
+
* entry point for that coverage.
|
|
8
|
+
*/
|
|
9
|
+
import type { MarkdownLine, ScanWarning } from '../types.js';
|
|
10
|
+
export interface SkillsTreeScanInput {
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SkillsTreeScanResult {
|
|
14
|
+
readonly lines: readonly MarkdownLine[];
|
|
15
|
+
readonly warnings: readonly ScanWarning[];
|
|
16
|
+
}
|
|
17
|
+
export declare function scanSkillsTree(input: SkillsTreeScanInput): SkillsTreeScanResult;
|