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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2-a Theme E — reference integrity enforcers.
|
|
3
|
+
*
|
|
4
|
+
* Static pattern scans of skill bodies + references/ for inline
|
|
5
|
+
* shell snippets that violate the project's "no
|
|
6
|
+
* mkdir-outside-project" / "no /tmp cp" / "no cd .." rules.
|
|
7
|
+
*
|
|
8
|
+
* These are pattern-based: the audit framework invokes the helper
|
|
9
|
+
* with a skill's body and the helper returns lint hits.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
const MKDIR_PATTERN = /\bmkdir\s+(?:-p\s+)?(\/[^\s'`"]+|[A-Z]:[^\s'`"]+)/g;
|
|
14
|
+
const CD_OUT_PATTERN = /\bcd\s+(\.\.[\\/]|[A-Z]:[\\/])/g;
|
|
15
|
+
const CP_MV_LN_TMP_PATTERN = /\b(cp|mv|ln)\b[^\n]*\/tmp\b/g;
|
|
16
|
+
/** Theme E — reference integrity. */
|
|
17
|
+
export function lintRefPathResolves(skillsRoot, name, refs) {
|
|
18
|
+
const hits = [];
|
|
19
|
+
for (const ref of refs) {
|
|
20
|
+
const path = join(skillsRoot, name, 'references', ref);
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
hits.push({
|
|
23
|
+
catalogId: 'rl-ref-path-resolves-001',
|
|
24
|
+
rule: 'every references/<file>.md link resolves',
|
|
25
|
+
file: join(skillsRoot, name, 'SKILL.md'),
|
|
26
|
+
line: 1,
|
|
27
|
+
matchedText: ref
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return hits;
|
|
32
|
+
}
|
|
33
|
+
export function lintNoBrokenMkdir(skill) {
|
|
34
|
+
const hits = [];
|
|
35
|
+
for (let i = 0; i < skill.lines.length; i += 1) {
|
|
36
|
+
const line = skill.lines[i] ?? '';
|
|
37
|
+
if (MKDIR_PATTERN.test(line)) {
|
|
38
|
+
hits.push({
|
|
39
|
+
catalogId: 'rl-ref-no-broken-mkdir-001',
|
|
40
|
+
rule: 'no `mkdir -p` outside the project root',
|
|
41
|
+
file: skill.path,
|
|
42
|
+
line: i + 1,
|
|
43
|
+
matchedText: line.trim()
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
MKDIR_PATTERN.lastIndex = 0;
|
|
47
|
+
}
|
|
48
|
+
return hits;
|
|
49
|
+
}
|
|
50
|
+
export function lintNoPwdSymlinkJumps(skill) {
|
|
51
|
+
const hits = [];
|
|
52
|
+
for (let i = 0; i < skill.lines.length; i += 1) {
|
|
53
|
+
const line = skill.lines[i] ?? '';
|
|
54
|
+
if (CD_OUT_PATTERN.test(line)) {
|
|
55
|
+
hits.push({
|
|
56
|
+
catalogId: 'rl-ref-no-pwd-symlink-jumps-001',
|
|
57
|
+
rule: 'no `cd ..` chain jumping outside the project',
|
|
58
|
+
file: skill.path,
|
|
59
|
+
line: i + 1,
|
|
60
|
+
matchedText: line.trim()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
CD_OUT_PATTERN.lastIndex = 0;
|
|
64
|
+
}
|
|
65
|
+
return hits;
|
|
66
|
+
}
|
|
67
|
+
export function lintNoRelativeArchivePaths(skill) {
|
|
68
|
+
const hits = [];
|
|
69
|
+
for (let i = 0; i < skill.lines.length; i += 1) {
|
|
70
|
+
const line = skill.lines[i] ?? '';
|
|
71
|
+
if (CP_MV_LN_TMP_PATTERN.test(line)) {
|
|
72
|
+
hits.push({
|
|
73
|
+
catalogId: 'rl-ref-no-relative-archive-paths-001',
|
|
74
|
+
rule: 'no `cp`/`mv`/`ln` to absolute /tmp paths',
|
|
75
|
+
file: skill.path,
|
|
76
|
+
line: i + 1,
|
|
77
|
+
matchedText: line.trim()
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
CP_MV_LN_TMP_PATTERN.lastIndex = 0;
|
|
81
|
+
}
|
|
82
|
+
return hits;
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LintHit, SkillFile } from './lint-style.js';
|
|
2
|
+
export interface ReferenceFile {
|
|
3
|
+
readonly skill: string;
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
readonly lines: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function lintH1TitleRequired(ref: ReferenceFile): readonly LintHit[];
|
|
10
|
+
export declare function lintApplicableTaskLevels(ref: ReferenceFile): readonly LintHit[];
|
|
11
|
+
export declare function lintSeeAlsoSection(ref: ReferenceFile): readonly LintHit[];
|
|
12
|
+
export declare function lintCrossRefResolves(ref: ReferenceFile, refsDir: string, siblings: readonly string[]): readonly LintHit[];
|
|
13
|
+
export declare function lintNoSelfReference(ref: ReferenceFile): readonly LintHit[];
|
|
14
|
+
export declare function lintNoOrphanLink(ref: ReferenceFile): readonly LintHit[];
|
|
15
|
+
export declare function lintLineCountLe800(ref: ReferenceFile): readonly LintHit[];
|
|
16
|
+
export declare function lintH2CountLe12(ref: ReferenceFile): readonly LintHit[];
|
|
17
|
+
export declare function lintOverviewNearTop(ref: ReferenceFile): readonly LintHit[];
|
|
18
|
+
export declare function lintLoadStrategyOnDemandFallback(ref: ReferenceFile): readonly LintHit[];
|
|
19
|
+
export declare function lintLoadStrategyAlwaysCacheable(ref: ReferenceFile): readonly LintHit[];
|
|
20
|
+
export declare function lintNoBashHeredoc(ref: ReferenceFile): readonly LintHit[];
|
|
21
|
+
export declare function lintNoSudo(ref: ReferenceFile): readonly LintHit[];
|
|
22
|
+
export declare function lintNoCurlPipeBash(ref: ReferenceFile): readonly LintHit[];
|
|
23
|
+
export declare function lintCodeBlockLanguage(ref: ReferenceFile): readonly LintHit[];
|
|
24
|
+
export declare function lintNoFakePrompt(ref: ReferenceFile): readonly LintHit[];
|
|
25
|
+
export declare function lintNoAbsolutePaths(ref: ReferenceFile): readonly LintHit[];
|
|
26
|
+
export declare function lintNoChmod777(ref: ReferenceFile): readonly LintHit[];
|
|
27
|
+
export declare function lintNoMagicNumbers(ref: ReferenceFile): readonly LintHit[];
|
|
28
|
+
export declare function lintSkillCitesEveryReference(ref: ReferenceFile, skill: SkillFile): readonly LintHit[];
|
|
29
|
+
export declare function lintLoadStrategyMatchesSize(ref: ReferenceFile): readonly LintHit[];
|
|
30
|
+
export declare function readReferenceFiles(skillsRoot: string, skillName: string, refNames: readonly string[]): readonly ReferenceFile[];
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2-b Themes H-K + M-P — references/*.md shape enforcers.
|
|
3
|
+
*
|
|
4
|
+
* Slice #7 L2.4. Each enforcer is a pure function that walks
|
|
5
|
+
* `references/*.md` and returns `readonly LintHit[]`. The audit
|
|
6
|
+
* service (`red-lines-service.ts`) is responsible for the
|
|
7
|
+
* walk; this file is pattern-only.
|
|
8
|
+
*
|
|
9
|
+
* Themes covered here:
|
|
10
|
+
* H — reference structural shape (3 enforcers)
|
|
11
|
+
* I — reference cross-references (3 enforcers)
|
|
12
|
+
* J — reference size + structure (3 enforcers)
|
|
13
|
+
* K — loadStrategy behavior (2 enforcers)
|
|
14
|
+
* M — inline shell patterns (3 enforcers)
|
|
15
|
+
* N — code blocks (3 enforcers)
|
|
16
|
+
* O — permissions + numbers (2 enforcers)
|
|
17
|
+
* P — dogfooding (2 enforcers)
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
const H1_TITLE_PATTERN = /^#\s+\S/m;
|
|
22
|
+
const TASK_LEVELS_PATTERN = /(applicableTaskLevels|task levels:|applies to (L1a|L1b|L2|L3|L4))/i;
|
|
23
|
+
const SEE_ALSO_HEADING = /^##\s+(See also|Related|References)\b/im;
|
|
24
|
+
const H2_HEADING = /^##\s+\S/gm;
|
|
25
|
+
const OVERVIEW_HEADING = /^##\s+Overview\b/im;
|
|
26
|
+
const FALLBACK_PATTERN = /(> Fallback:|^\*\*Fallback\*\*:)/m;
|
|
27
|
+
const TOP_LEVEL_SUDO = /^\s*sudo\s+/m;
|
|
28
|
+
const CURL_PIPE_BASH = /curl[^\n]*\|\s*bash/;
|
|
29
|
+
const HEREDOC_PATTERN = /<<-?\s*\w+/;
|
|
30
|
+
const CHMOD_777 = /chmod\s+777/;
|
|
31
|
+
const FENCED_BLOCK = /```(\w*)\n([\s\S]*?)\n```/g;
|
|
32
|
+
const FENCED_BLOCK_LANG_REQUIRED = /```\w+\n/;
|
|
33
|
+
const FAKE_PROMPT = /^[\s]*(?:#|\$)\s*fake\b/im;
|
|
34
|
+
const ABSOLUTE_PATH_WINDOWS = /[A-Z]:\\[\w\\]/;
|
|
35
|
+
const ABSOLUTE_PATH_UNIX = /\/usr\/(?:local|bin|opt)\b/;
|
|
36
|
+
const MAGIC_NUMBER = /\b(\d{3,})\b/;
|
|
37
|
+
const LOAD_STRATEGY_PATTERN = /loadStrategy:\s*(always|on-demand)/i;
|
|
38
|
+
function findLine(lines, pattern) {
|
|
39
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
40
|
+
if (pattern.test(lines[i] ?? ''))
|
|
41
|
+
return i + 1;
|
|
42
|
+
}
|
|
43
|
+
return -1;
|
|
44
|
+
}
|
|
45
|
+
function hit(catalogId, rule, file, line, matchedText) {
|
|
46
|
+
return { catalogId, rule, file, line, matchedText };
|
|
47
|
+
}
|
|
48
|
+
// ==================== Theme H — structural shape ====================
|
|
49
|
+
export function lintH1TitleRequired(ref) {
|
|
50
|
+
if (H1_TITLE_PATTERN.test(ref.body))
|
|
51
|
+
return [];
|
|
52
|
+
return [hit('rl-ref-h1-title-required-001', 'every references/*.md starts with `# <title>`', ref.path, 1, '(missing `# <title>` first-line heading)')];
|
|
53
|
+
}
|
|
54
|
+
export function lintApplicableTaskLevels(ref) {
|
|
55
|
+
if (TASK_LEVELS_PATTERN.test(ref.body))
|
|
56
|
+
return [];
|
|
57
|
+
return [hit('rl-ref-applicable-task-levels-declared-001', 'every references/*.md declares applicableTaskLevels', ref.path, 1, '(missing applicableTaskLevels declaration)')];
|
|
58
|
+
}
|
|
59
|
+
export function lintSeeAlsoSection(ref) {
|
|
60
|
+
if (SEE_ALSO_HEADING.test(ref.body))
|
|
61
|
+
return [];
|
|
62
|
+
return [hit('rl-ref-see-also-section-001', 'every references/*.md has a `## See also` section', ref.path, 1, '(missing `## See also` (or `## Related` / `## References`) section)')];
|
|
63
|
+
}
|
|
64
|
+
// ==================== Theme I — cross-references ====================
|
|
65
|
+
export function lintCrossRefResolves(ref, refsDir, siblings) {
|
|
66
|
+
const hits = [];
|
|
67
|
+
// Match `../<file>.md` or `references/<file>.md` or `<file>.md` style.
|
|
68
|
+
const linkPattern = /\[([^\]]+)\]\((?:\.\/|\.\.\/)?(?:references\/)?([\w./-]+\.md)(?:#[\w-]+)?\)/g;
|
|
69
|
+
let m;
|
|
70
|
+
while ((m = linkPattern.exec(ref.body)) !== null) {
|
|
71
|
+
const target = m[2] ?? '';
|
|
72
|
+
if (!target)
|
|
73
|
+
continue;
|
|
74
|
+
const candidates = [
|
|
75
|
+
join(refsDir, target),
|
|
76
|
+
join(refsDir, '..', target),
|
|
77
|
+
join(refsDir, '..', '..', target),
|
|
78
|
+
];
|
|
79
|
+
const exists = candidates.some((c) => existsSync(c)) || siblings.includes(target);
|
|
80
|
+
if (!exists) {
|
|
81
|
+
const line = ref.body.slice(0, m.index ?? 0).split('\n').length;
|
|
82
|
+
hits.push(hit('rl-ref-cross-ref-resolves-001', 'every `../<file>.md` link from a reference resolves', ref.path, line, target));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return hits;
|
|
86
|
+
}
|
|
87
|
+
export function lintNoSelfReference(ref) {
|
|
88
|
+
const basename = ref.name;
|
|
89
|
+
// Match `[text](<basename>)` (exact or with section fragment).
|
|
90
|
+
const re = new RegExp(`\\]\\(${basename}(?:#[\\w-]+)?\\)`);
|
|
91
|
+
if (!re.test(ref.body))
|
|
92
|
+
return [];
|
|
93
|
+
const line = findLine(ref.lines, re);
|
|
94
|
+
return [hit('rl-ref-no-self-reference-001', 'no reference file links to itself', ref.path, line === -1 ? 1 : line, `(self-link to ${basename})`)];
|
|
95
|
+
}
|
|
96
|
+
export function lintNoOrphanLink(ref) {
|
|
97
|
+
// Heuristic: a markdown link to a non-.md URL with no protocol
|
|
98
|
+
// and no recognized image extension is an orphan. We do a soft
|
|
99
|
+
// pass here — the strict version is `lintCrossRefResolves`
|
|
100
|
+
// above; this one is a defensive backstop for paths the strict
|
|
101
|
+
// version doesn't catch.
|
|
102
|
+
const hits = [];
|
|
103
|
+
const linkPattern = /\[([^\]]+)\]\(([\w./-]+)\)/g;
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = linkPattern.exec(ref.body)) !== null) {
|
|
106
|
+
const target = m[2] ?? '';
|
|
107
|
+
if (!target || target.startsWith('http') || target.startsWith('mailto:'))
|
|
108
|
+
continue;
|
|
109
|
+
if (target.endsWith('.md') || target.endsWith('.json') || target.endsWith('.ts'))
|
|
110
|
+
continue;
|
|
111
|
+
// Bare word: probably an anchor link; skip.
|
|
112
|
+
if (!target.includes('/') && !target.includes('.'))
|
|
113
|
+
continue;
|
|
114
|
+
const line = ref.body.slice(0, m.index ?? 0).split('\n').length;
|
|
115
|
+
hits.push(hit('rl-ref-no-orphan-link-001', 'no link to a non-existent file or section', ref.path, line, target));
|
|
116
|
+
}
|
|
117
|
+
return hits;
|
|
118
|
+
}
|
|
119
|
+
// ==================== Theme J — size + structure ====================
|
|
120
|
+
export function lintLineCountLe800(ref) {
|
|
121
|
+
if (ref.lines.length <= 800)
|
|
122
|
+
return [];
|
|
123
|
+
return [hit('rl-ref-line-count-le-800-001', 'each reference ≤ 800 lines (Karpathy 4 原则 §2.3)', ref.path, 1, `(line count ${ref.lines.length} > 800)`)];
|
|
124
|
+
}
|
|
125
|
+
export function lintH2CountLe12(ref) {
|
|
126
|
+
const matches = ref.body.match(H2_HEADING) ?? [];
|
|
127
|
+
if (matches.length <= 12)
|
|
128
|
+
return [];
|
|
129
|
+
return [hit('rl-ref-h2-count-le-12-001', 'at most 12 `## <heading>` per reference', ref.path, 1, `(h2 count ${matches.length} > 12)`)];
|
|
130
|
+
}
|
|
131
|
+
export function lintOverviewNearTop(ref) {
|
|
132
|
+
if (ref.lines.length <= 200)
|
|
133
|
+
return [];
|
|
134
|
+
if (findLine(ref.lines.slice(0, 30), OVERVIEW_HEADING) !== -1)
|
|
135
|
+
return [];
|
|
136
|
+
return [hit('rl-ref-overview-section-near-top-001', 'long references (>200 lines) must have `## Overview` within the first 30 lines', ref.path, 1, '(missing `## Overview` near top of long reference)')];
|
|
137
|
+
}
|
|
138
|
+
// ==================== Theme K — loadStrategy behavior ====================
|
|
139
|
+
export function lintLoadStrategyOnDemandFallback(ref) {
|
|
140
|
+
if (!/loadStrategy:\s*on-demand/i.test(ref.body))
|
|
141
|
+
return [];
|
|
142
|
+
if (FALLBACK_PATTERN.test(ref.body))
|
|
143
|
+
return [];
|
|
144
|
+
return [hit('rl-ref-loadstrategy-on-demand-fallback-001', 'loadStrategy: on-demand references must declare a fallback path', ref.path, 1, '(missing `> Fallback:` or `**Fallback**:` declaration)')];
|
|
145
|
+
}
|
|
146
|
+
export function lintLoadStrategyAlwaysCacheable(ref) {
|
|
147
|
+
if (!/loadStrategy:\s*always/i.test(ref.body))
|
|
148
|
+
return [];
|
|
149
|
+
// Heuristic: a loadStrategy: always reference must not have a
|
|
150
|
+
// top-level shell command. This is a soft check — we look at
|
|
151
|
+
// the first 10 non-frontmatter lines for I/O patterns.
|
|
152
|
+
const lines = ref.lines.slice(0, 20);
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
if (/^\s*(npm|pnpm|yarn|npx|git|curl|wget|docker)\s+/.test(line)) {
|
|
155
|
+
return [hit('rl-ref-loadstrategy-always-cacheable-001', 'loadStrategy: always references must not run I/O at top of file', ref.path, 1, `(I/O pattern at top of file: ${line.trim()})`)];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
// ==================== Theme M — inline shell patterns ====================
|
|
161
|
+
export function lintNoBashHeredoc(ref) {
|
|
162
|
+
if (!HEREDOC_PATTERN.test(ref.body))
|
|
163
|
+
return [];
|
|
164
|
+
const line = findLine(ref.lines, HEREDOC_PATTERN);
|
|
165
|
+
return [hit('rl-ref-no-bash-heredoc-001', 'no `cat <<EOF` in inline shell snippets', ref.path, line === -1 ? 1 : line, '(bash heredoc pattern found)')];
|
|
166
|
+
}
|
|
167
|
+
export function lintNoSudo(ref) {
|
|
168
|
+
if (!TOP_LEVEL_SUDO.test(ref.body))
|
|
169
|
+
return [];
|
|
170
|
+
const line = findLine(ref.lines, TOP_LEVEL_SUDO);
|
|
171
|
+
return [hit('rl-ref-no-sudo-001', 'no `sudo` in inline shell snippets', ref.path, line === -1 ? 1 : line, '(sudo command found)')];
|
|
172
|
+
}
|
|
173
|
+
export function lintNoCurlPipeBash(ref) {
|
|
174
|
+
if (!CURL_PIPE_BASH.test(ref.body))
|
|
175
|
+
return [];
|
|
176
|
+
const line = findLine(ref.lines, CURL_PIPE_BASH);
|
|
177
|
+
return [hit('rl-ref-no-curl-pipe-bash-001', 'no `curl ... | bash` in inline shell snippets', ref.path, line === -1 ? 1 : line, '(curl-pipe-bash pattern found)')];
|
|
178
|
+
}
|
|
179
|
+
// ==================== Theme N — code blocks ====================
|
|
180
|
+
export function lintCodeBlockLanguage(ref) {
|
|
181
|
+
const hits = [];
|
|
182
|
+
// Reset regex state.
|
|
183
|
+
FENCED_BLOCK.lastIndex = 0;
|
|
184
|
+
let m;
|
|
185
|
+
while ((m = FENCED_BLOCK.exec(ref.body)) !== null) {
|
|
186
|
+
const lang = m[1] ?? '';
|
|
187
|
+
if (lang === '') {
|
|
188
|
+
const line = ref.body.slice(0, m.index ?? 0).split('\n').length;
|
|
189
|
+
hits.push(hit('rl-ref-code-block-language-declared-001', 'every fenced block has a language tag', ref.path, line, '(untyped fenced code block — ` ``` ` without language tag)'));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return hits;
|
|
193
|
+
}
|
|
194
|
+
export function lintNoFakePrompt(ref) {
|
|
195
|
+
if (!FAKE_PROMPT.test(ref.body))
|
|
196
|
+
return [];
|
|
197
|
+
const line = findLine(ref.lines, FAKE_PROMPT);
|
|
198
|
+
return [hit('rl-ref-no-fake-prompt-001', 'no `# fake prompt` / `$ fake` markers in code blocks', ref.path, line === -1 ? 1 : line, '(fake-prompt marker found)')];
|
|
199
|
+
}
|
|
200
|
+
export function lintNoAbsolutePaths(ref) {
|
|
201
|
+
const hits = [];
|
|
202
|
+
if (ABSOLUTE_PATH_WINDOWS.test(ref.body)) {
|
|
203
|
+
const line = findLine(ref.lines, ABSOLUTE_PATH_WINDOWS);
|
|
204
|
+
hits.push(hit('rl-ref-no-absolute-paths-001', 'no `C:\\` or `/usr/local` in code blocks', ref.path, line === -1 ? 1 : line, '(Windows absolute path found)'));
|
|
205
|
+
}
|
|
206
|
+
if (ABSOLUTE_PATH_UNIX.test(ref.body)) {
|
|
207
|
+
const line = findLine(ref.lines, ABSOLUTE_PATH_UNIX);
|
|
208
|
+
hits.push(hit('rl-ref-no-absolute-paths-001', 'no `C:\\` or `/usr/local` in code blocks', ref.path, line === -1 ? 1 : line, '(Unix absolute path found)'));
|
|
209
|
+
}
|
|
210
|
+
return hits;
|
|
211
|
+
}
|
|
212
|
+
// ==================== Theme O — permissions + numbers ====================
|
|
213
|
+
export function lintNoChmod777(ref) {
|
|
214
|
+
if (!CHMOD_777.test(ref.body))
|
|
215
|
+
return [];
|
|
216
|
+
const line = findLine(ref.lines, CHMOD_777);
|
|
217
|
+
return [hit('rl-ref-no-chmod-777-001', 'no `chmod 777` in inline shell', ref.path, line === -1 ? 1 : line, '(chmod 777 found — security red flag)')];
|
|
218
|
+
}
|
|
219
|
+
export function lintNoMagicNumbers(ref) {
|
|
220
|
+
// Look at code blocks for unsigned integers ≥ 100. We don't
|
|
221
|
+
// try to be smart about hex / decimal distinction; the
|
|
222
|
+
// enforcer is pattern-only.
|
|
223
|
+
const hits = [];
|
|
224
|
+
FENCED_BLOCK.lastIndex = 0;
|
|
225
|
+
let m;
|
|
226
|
+
while ((m = FENCED_BLOCK.exec(ref.body)) !== null) {
|
|
227
|
+
const block = m[2] ?? '';
|
|
228
|
+
const numMatch = block.match(MAGIC_NUMBER);
|
|
229
|
+
if (!numMatch)
|
|
230
|
+
continue;
|
|
231
|
+
const offset = (m.index ?? 0) + (m[0].indexOf(numMatch[0] ?? ''));
|
|
232
|
+
const line = ref.body.slice(0, offset).split('\n').length;
|
|
233
|
+
hits.push(hit('rl-ref-no-magic-numbers-001', 'no unsigned integer ≥ 100 that is not a named constant', ref.path, line, `(magic number ${numMatch[0]} in code block)`));
|
|
234
|
+
}
|
|
235
|
+
return hits;
|
|
236
|
+
}
|
|
237
|
+
// ==================== Theme P — dogfooding ====================
|
|
238
|
+
export function lintSkillCitesEveryReference(ref, skill) {
|
|
239
|
+
// A reference IS cited if its name appears anywhere in the
|
|
240
|
+
// parent SKILL.md body, OR if its content includes a forward
|
|
241
|
+
// link to the SKILL.md.
|
|
242
|
+
const refName = ref.name;
|
|
243
|
+
const skillName = skill.name;
|
|
244
|
+
const citedInSkill = skill.body.includes(refName) || skill.body.includes(`./references/${refName}`);
|
|
245
|
+
const citedInRef = ref.body.includes(`../SKILL.md`) || ref.body.includes(`SKILL.md#`);
|
|
246
|
+
if (citedInSkill || citedInRef)
|
|
247
|
+
return [];
|
|
248
|
+
return [hit('rl-ref-skill-cites-every-existing-reference-001', 'every reference IS cited in its parent SKILL.md (or links to it)', ref.path, 1, `(uncited reference ${refName} in skill ${skillName})`)];
|
|
249
|
+
}
|
|
250
|
+
export function lintLoadStrategyMatchesSize(ref) {
|
|
251
|
+
const sizeBytes = Buffer.byteLength(ref.body, 'utf8');
|
|
252
|
+
if (sizeBytes <= 5 * 1024)
|
|
253
|
+
return [];
|
|
254
|
+
// >5KB file should declare loadStrategy: on-demand (always
|
|
255
|
+
// is a context-budget bug).
|
|
256
|
+
const strategy = LOAD_STRATEGY_PATTERN.exec(ref.body);
|
|
257
|
+
if (strategy && strategy[1]?.toLowerCase() === 'on-demand')
|
|
258
|
+
return [];
|
|
259
|
+
return [hit('rl-ref-loadstrategy-matches-size-001', 'loadStrategy: on-demand is required for files > 5KB', ref.path, 1, `(size ${sizeBytes} bytes; loadStrategy must be \`on-demand\`)`)];
|
|
260
|
+
}
|
|
261
|
+
export function readReferenceFiles(skillsRoot, skillName, refNames) {
|
|
262
|
+
const refsDir = join(skillsRoot, skillName, 'references');
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const name of refNames) {
|
|
265
|
+
const path = join(refsDir, name);
|
|
266
|
+
if (!existsSync(path))
|
|
267
|
+
continue;
|
|
268
|
+
const body = readFileSync(path, 'utf8');
|
|
269
|
+
out.push({ skill: skillName, name, path, body, lines: body.split(/\r?\n/) });
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface LintHit {
|
|
2
|
+
readonly catalogId: string;
|
|
3
|
+
readonly rule: string;
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly matchedText: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SkillFile {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly body: string;
|
|
12
|
+
readonly lines: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function readSkillFiles(skillsRoot: string, names: readonly string[]): readonly SkillFile[];
|
|
15
|
+
/** Theme A — section structure. Returns lint hits (positive = rule
|
|
16
|
+
* satisfied, so a missing heading fires the lint hit; downstream
|
|
17
|
+
* audit service decides whether to WARN or pass). */
|
|
18
|
+
export declare function lintSectionShape(skill: SkillFile): readonly LintHit[];
|
|
19
|
+
/**
|
|
20
|
+
* ASCII wireframe section-order check (spec §5.4 line 647).
|
|
21
|
+
*
|
|
22
|
+
* The "ASCII wireframe" lint hint in the spec means: SKILL.md
|
|
23
|
+
* follows a canonical section ordering. The first 5 sections
|
|
24
|
+
* must appear in this fixed order:
|
|
25
|
+
*
|
|
26
|
+
* 1. Two-axis naming axiom (top-of-file naming convention callout)
|
|
27
|
+
* 2. Hard contracts for browser/IO surface (when present)
|
|
28
|
+
* 3. Mandatory per-request artifact
|
|
29
|
+
* 4. Default runbook
|
|
30
|
+
* 5. RD / QA gate index
|
|
31
|
+
*
|
|
32
|
+
* If both sections exist but appear in the wrong order, the
|
|
33
|
+
* enforcer reports a wireframe-violation hit pointing at the
|
|
34
|
+
* section that came too early.
|
|
35
|
+
*
|
|
36
|
+
* If a section is missing, the per-section enforcer (above)
|
|
37
|
+
* already fires; this one only handles the **order** invariant
|
|
38
|
+
* (the wireframe shape).
|
|
39
|
+
*/
|
|
40
|
+
export declare function lintSectionOrder(skill: SkillFile): readonly LintHit[];
|
|
41
|
+
/** Theme B — frontmatter shape. Same convention as Theme A. */
|
|
42
|
+
export declare function lintFrontmatterShape(skill: SkillFile): readonly LintHit[];
|
|
43
|
+
/**
|
|
44
|
+
* Reference loadStrategy: scan every `references/*.md` in the
|
|
45
|
+
* skill's references/ dir for a `loadStrategy: always | on-demand`
|
|
46
|
+
* line. The audit is invoked per-skill, so this helper takes a
|
|
47
|
+
* `referencesRoot` (the skill's `references/` dir).
|
|
48
|
+
*/
|
|
49
|
+
export declare function lintReferenceLoadStrategy(referencesRoot: string, refs: readonly string[]): readonly LintHit[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2-a lint-style enforcers — Theme A (section structure) and
|
|
3
|
+
* Theme B (frontmatter shape).
|
|
4
|
+
*
|
|
5
|
+
* These enforcers do NOT block commands; they feed the
|
|
6
|
+
* `peaks audit red-lines --json` report. The backing-detector
|
|
7
|
+
* downgrades them to `cli-backed` when the catalog has an
|
|
8
|
+
* `enforcerRef` (i.e. this file) and the per-skill scan finds a
|
|
9
|
+
* match. The audit service counts hits + misses and reports the
|
|
10
|
+
* per-catalog-entry breakdown.
|
|
11
|
+
*
|
|
12
|
+
* Pattern-only (no shell-out, no FS writes, no env mutations) so
|
|
13
|
+
* the enforcer can run inside the `peaks audit red-lines` hot
|
|
14
|
+
* path without the 5s ECC-AgentShield subprocess budget.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
const SECTION_HARD_CONTRACTS_HEADING = /^##\s+(Hard contracts|Hard contract)\b/im;
|
|
19
|
+
const SECTION_MANDATORY_HEADING = /^##\s+Mandatory\b/im;
|
|
20
|
+
const SECTION_DEFAULT_RUNBOOK_HEADING = /^##\s+(Default runbook|Default)\b/im;
|
|
21
|
+
const SECTION_GATE_INDEX_HEADING = /^##\s+(RD gate index|QA gate index|Gate index|gate-index)\b/im;
|
|
22
|
+
const SECTION_NAMING_AXIOM_HEADING = /(Two-axis naming convention|change-id.*session-id|두 가지 직교 축)/i;
|
|
23
|
+
const FRONTMATTER_NAME_LINE = /^name:\s*peaks-/m;
|
|
24
|
+
const FRONTMATTER_DESCRIPTION_LINE = /^description:\s*\S/m;
|
|
25
|
+
const FRONTMATTER_LOAD_STRATEGY = /loadStrategy:\s*(always|on-demand)/i;
|
|
26
|
+
const FRONTMATTER_APPLICABLE_TASK_LEVELS = /applicableTaskLevels/i;
|
|
27
|
+
export function readSkillFiles(skillsRoot, names) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const path = join(skillsRoot, name, 'SKILL.md');
|
|
31
|
+
const body = readFileSync(path, 'utf8');
|
|
32
|
+
out.push({ name, path, body, lines: body.split(/\r?\n/) });
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function findLine(lines, pattern) {
|
|
37
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
38
|
+
if (pattern.test(lines[i] ?? ''))
|
|
39
|
+
return i + 1;
|
|
40
|
+
}
|
|
41
|
+
return -1;
|
|
42
|
+
}
|
|
43
|
+
function matchedText(lines, line) {
|
|
44
|
+
if (line <= 0)
|
|
45
|
+
return '';
|
|
46
|
+
return (lines[line - 1] ?? '').trim();
|
|
47
|
+
}
|
|
48
|
+
/** Theme A — section structure. Returns lint hits (positive = rule
|
|
49
|
+
* satisfied, so a missing heading fires the lint hit; downstream
|
|
50
|
+
* audit service decides whether to WARN or pass). */
|
|
51
|
+
export function lintSectionShape(skill) {
|
|
52
|
+
const hits = [];
|
|
53
|
+
const rules = [
|
|
54
|
+
{ id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING },
|
|
55
|
+
{ id: 'rl-section-mandatory-artifact-001', rule: 'Mandatory per-request artifact', pattern: SECTION_MANDATORY_HEADING },
|
|
56
|
+
{ id: 'rl-section-default-runbook-001', rule: 'Default runbook pointer', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
|
|
57
|
+
{ id: 'rl-section-gate-index-001', rule: 'Gate index', pattern: SECTION_GATE_INDEX_HEADING },
|
|
58
|
+
{ id: 'rl-section-naming-axiom-001', rule: 'Two-axis naming axiom', pattern: SECTION_NAMING_AXIOM_HEADING }
|
|
59
|
+
];
|
|
60
|
+
for (const r of rules) {
|
|
61
|
+
const line = findLine(skill.lines, r.pattern);
|
|
62
|
+
if (line === -1) {
|
|
63
|
+
hits.push({
|
|
64
|
+
catalogId: r.id,
|
|
65
|
+
rule: r.rule,
|
|
66
|
+
file: skill.path,
|
|
67
|
+
line: 1,
|
|
68
|
+
matchedText: '(missing section)'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return hits;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* ASCII wireframe section-order check (spec §5.4 line 647).
|
|
76
|
+
*
|
|
77
|
+
* The "ASCII wireframe" lint hint in the spec means: SKILL.md
|
|
78
|
+
* follows a canonical section ordering. The first 5 sections
|
|
79
|
+
* must appear in this fixed order:
|
|
80
|
+
*
|
|
81
|
+
* 1. Two-axis naming axiom (top-of-file naming convention callout)
|
|
82
|
+
* 2. Hard contracts for browser/IO surface (when present)
|
|
83
|
+
* 3. Mandatory per-request artifact
|
|
84
|
+
* 4. Default runbook
|
|
85
|
+
* 5. RD / QA gate index
|
|
86
|
+
*
|
|
87
|
+
* If both sections exist but appear in the wrong order, the
|
|
88
|
+
* enforcer reports a wireframe-violation hit pointing at the
|
|
89
|
+
* section that came too early.
|
|
90
|
+
*
|
|
91
|
+
* If a section is missing, the per-section enforcer (above)
|
|
92
|
+
* already fires; this one only handles the **order** invariant
|
|
93
|
+
* (the wireframe shape).
|
|
94
|
+
*/
|
|
95
|
+
export function lintSectionOrder(skill) {
|
|
96
|
+
const hits = [];
|
|
97
|
+
const order = [
|
|
98
|
+
{ id: 'rl-section-naming-axiom-001', rule: 'Two-axis naming axiom must precede Hard contracts', pattern: SECTION_NAMING_AXIOM_HEADING },
|
|
99
|
+
{ id: 'rl-section-hard-contracts-001', rule: 'Hard contracts must precede Default runbook', pattern: SECTION_HARD_CONTRACTS_HEADING },
|
|
100
|
+
{ id: 'rl-section-default-runbook-001', rule: 'Default runbook must precede Gate index', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
|
|
101
|
+
];
|
|
102
|
+
let lastSeenLine = -1;
|
|
103
|
+
let lastSeenRule = '';
|
|
104
|
+
for (const r of order) {
|
|
105
|
+
const line = findLine(skill.lines, r.pattern);
|
|
106
|
+
if (line === -1)
|
|
107
|
+
continue; // missing-section is its own enforcer
|
|
108
|
+
if (line < lastSeenLine) {
|
|
109
|
+
hits.push({
|
|
110
|
+
catalogId: 'rl-section-order-wireframe-001',
|
|
111
|
+
rule: 'Section wireframe order: each section must appear in the canonical order',
|
|
112
|
+
file: skill.path,
|
|
113
|
+
line,
|
|
114
|
+
matchedText: `(section "${r.rule}" at line ${line} comes after "${lastSeenRule}" at line ${lastSeenLine})`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
lastSeenLine = line;
|
|
118
|
+
lastSeenRule = r.rule;
|
|
119
|
+
}
|
|
120
|
+
return hits;
|
|
121
|
+
}
|
|
122
|
+
/** Theme B — frontmatter shape. Same convention as Theme A. */
|
|
123
|
+
export function lintFrontmatterShape(skill) {
|
|
124
|
+
const hits = [];
|
|
125
|
+
const hasName = FRONTMATTER_NAME_LINE.test(skill.body);
|
|
126
|
+
const hasDescription = FRONTMATTER_DESCRIPTION_LINE.test(skill.body);
|
|
127
|
+
if (!hasName || !hasDescription) {
|
|
128
|
+
hits.push({
|
|
129
|
+
catalogId: 'rl-frontmatter-skills-md-001',
|
|
130
|
+
rule: 'skills_md parseable frontmatter',
|
|
131
|
+
file: skill.path,
|
|
132
|
+
line: 1,
|
|
133
|
+
matchedText: '(missing name or description)'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// The `applicableTaskLevels` field is a body-level annotation, not
|
|
137
|
+
// strictly YAML frontmatter. We accept either a top-of-file
|
|
138
|
+
// frontmatter `applicableTaskLevels:` line or an in-body "applies
|
|
139
|
+
// to <levels>" sentence.
|
|
140
|
+
if (!FRONTMATTER_APPLICABLE_TASK_LEVELS.test(skill.body)) {
|
|
141
|
+
hits.push({
|
|
142
|
+
catalogId: 'rl-frontmatter-applicable-task-levels-001',
|
|
143
|
+
rule: 'skill applicable task levels',
|
|
144
|
+
file: skill.path,
|
|
145
|
+
line: 1,
|
|
146
|
+
matchedText: '(missing applicableTaskLevels)'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return hits;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Reference loadStrategy: scan every `references/*.md` in the
|
|
153
|
+
* skill's references/ dir for a `loadStrategy: always | on-demand`
|
|
154
|
+
* line. The audit is invoked per-skill, so this helper takes a
|
|
155
|
+
* `referencesRoot` (the skill's `references/` dir).
|
|
156
|
+
*/
|
|
157
|
+
export function lintReferenceLoadStrategy(referencesRoot, refs) {
|
|
158
|
+
const hits = [];
|
|
159
|
+
for (const ref of refs) {
|
|
160
|
+
const path = join(referencesRoot, ref);
|
|
161
|
+
const body = readFileSync(path, 'utf8');
|
|
162
|
+
if (!FRONTMATTER_LOAD_STRATEGY.test(body)) {
|
|
163
|
+
hits.push({
|
|
164
|
+
catalogId: 'rl-frontmatter-references-load-strategy-001',
|
|
165
|
+
rule: 'references loadStrategy declared',
|
|
166
|
+
file: path,
|
|
167
|
+
line: 1,
|
|
168
|
+
matchedText: '(missing loadStrategy)'
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return hits;
|
|
173
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LintHit, SkillFile } from './lint-style.js';
|
|
2
|
+
export declare function lintOpenSpecAcceptanceBullets(projectRoot: string): readonly LintHit[];
|
|
3
|
+
export declare function lintOpenSpecSpecReference(projectRoot: string): readonly LintHit[];
|
|
4
|
+
export declare function lintTechDocPresenceShape(projectRoot: string): readonly LintHit[];
|
|
5
|
+
export declare function lintPeaksDoctorAcknowledged(skill: SkillFile): readonly LintHit[];
|