renma 0.5.1 → 0.6.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/CHANGELOG.md +118 -0
- package/README.md +29 -6
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +3 -0
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/readiness.d.ts.map +1 -1
- package/dist/commands/readiness.js +57 -18
- package/dist/commands/readiness.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostic-ids.d.ts +88 -0
- package/dist/diagnostic-ids.d.ts.map +1 -0
- package/dist/diagnostic-ids.js +87 -0
- package/dist/diagnostic-ids.js.map +1 -0
- package/dist/freshness.d.ts +9 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +27 -0
- package/dist/freshness.js.map +1 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +26 -0
- package/dist/metadata.js.map +1 -1
- package/dist/model.d.ts +3 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/repeated-context.d.ts +2 -1
- package/dist/repeated-context.d.ts.map +1 -1
- package/dist/repeated-context.js +11 -10
- package/dist/repeated-context.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +119 -43
- package/dist/rules.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +52 -9
- package/dist/scanner.js.map +1 -1
- package/dist/security-diagnostics.d.ts.map +1 -1
- package/dist/security-diagnostics.js +25 -24
- package/dist/security-diagnostics.js.map +1 -1
- package/dist/suppressions.d.ts +9 -0
- package/dist/suppressions.d.ts.map +1 -0
- package/dist/suppressions.js +84 -0
- package/dist/suppressions.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
package/dist/rules.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { addDaysIsoDate, isIsoDate, parseDayDuration, todayIsoDate, } from "./freshness.js";
|
|
4
|
+
import { DIAGNOSTIC_IDS } from "./diagnostic-ids.js";
|
|
3
5
|
import { runRuleRegistry } from "./rule-engine.js";
|
|
4
6
|
const SECRET_PATTERN = /\b(?:password|passwd|token|api[_-]?key|secret|credential|private[_-]?key)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{8,})/i;
|
|
5
7
|
const PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/;
|
|
@@ -184,9 +186,83 @@ function catalogDeclaredReferenceGovernanceFindings(catalog) {
|
|
|
184
186
|
...unknownReferenceFindings(catalog.dependencies, resolver),
|
|
185
187
|
...referenceDeprecatedAssetFindings(catalog.dependencies, resolver),
|
|
186
188
|
...orphanedContextAssetFindings(catalog.entries, catalog.dependencies, resolver),
|
|
189
|
+
...freshnessGovernanceFindings(catalog.entries),
|
|
187
190
|
];
|
|
188
191
|
return findings;
|
|
189
192
|
}
|
|
193
|
+
function freshnessGovernanceFindings(entries, today = todayIsoDate()) {
|
|
194
|
+
return entries
|
|
195
|
+
.filter((entry) => entry.kind === "context" || entry.kind === "skill")
|
|
196
|
+
.flatMap((entry) => [
|
|
197
|
+
...expiredAssetFindings(entry, today),
|
|
198
|
+
...reviewOverdueAssetFindings(entry, today),
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
function expiredAssetFindings(entry, today) {
|
|
202
|
+
const expiresAt = entry.metadata.expiresAt;
|
|
203
|
+
if (!expiresAt || !isIsoDate(expiresAt) || expiresAt >= today)
|
|
204
|
+
return [];
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
id: DIAGNOSTIC_IDS.MAINT_ASSET_EXPIRED,
|
|
208
|
+
title: "Asset freshness metadata is expired",
|
|
209
|
+
category: "maintenance",
|
|
210
|
+
severity: "medium",
|
|
211
|
+
confidence: "high",
|
|
212
|
+
evidence: metadataFieldFindingEvidence(entry, "expires_at", `expires_at: ${expiresAt}`),
|
|
213
|
+
whyItMatters: "Freshness metadata is a human review contract. Expired assets may contain guidance that has not been intentionally revalidated for current use.",
|
|
214
|
+
remediation: "Review the asset with a human owner, then update expires_at, last_reviewed_at, review_cycle, status, or dependent references as appropriate.",
|
|
215
|
+
constraints: [
|
|
216
|
+
"Do not infer freshness from file modification time.",
|
|
217
|
+
"Do not introduce runtime context selection.",
|
|
218
|
+
"Do not create prompt packages.",
|
|
219
|
+
],
|
|
220
|
+
verificationSteps: [
|
|
221
|
+
"Run renma scan.",
|
|
222
|
+
"Run renma catalog.",
|
|
223
|
+
"Confirm the freshness metadata reflects human review.",
|
|
224
|
+
],
|
|
225
|
+
llmHint: `Review ${entry.sourcePath} for stale assumptions, then update explicit freshness metadata only after human review.`,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
function reviewOverdueAssetFindings(entry, today) {
|
|
230
|
+
const lastReviewedAt = entry.metadata.lastReviewedAt;
|
|
231
|
+
const reviewCycle = entry.metadata.reviewCycle;
|
|
232
|
+
if (!lastReviewedAt || !reviewCycle)
|
|
233
|
+
return [];
|
|
234
|
+
if (!isIsoDate(lastReviewedAt))
|
|
235
|
+
return [];
|
|
236
|
+
const cycleDays = parseDayDuration(reviewCycle);
|
|
237
|
+
if (cycleDays === undefined)
|
|
238
|
+
return [];
|
|
239
|
+
const dueAt = addDaysIsoDate(lastReviewedAt, cycleDays);
|
|
240
|
+
if (dueAt >= today)
|
|
241
|
+
return [];
|
|
242
|
+
return [
|
|
243
|
+
{
|
|
244
|
+
id: DIAGNOSTIC_IDS.MAINT_ASSET_REVIEW_OVERDUE,
|
|
245
|
+
title: "Asset freshness review is overdue",
|
|
246
|
+
category: "maintenance",
|
|
247
|
+
severity: "medium",
|
|
248
|
+
confidence: "high",
|
|
249
|
+
evidence: metadataFieldFindingEvidence(entry, "last_reviewed_at", `last_reviewed_at: ${lastReviewedAt}; review_cycle: ${reviewCycle}`),
|
|
250
|
+
whyItMatters: "Review cycles make human freshness expectations explicit. Overdue assets may no longer match current product, policy, or workflow behavior.",
|
|
251
|
+
remediation: "Review the asset with a human owner, then update last_reviewed_at or review_cycle if the guidance is still current.",
|
|
252
|
+
constraints: [
|
|
253
|
+
"Do not infer freshness from Git history or file modification time.",
|
|
254
|
+
"Do not introduce runtime context selection.",
|
|
255
|
+
"Do not create prompt packages.",
|
|
256
|
+
],
|
|
257
|
+
verificationSteps: [
|
|
258
|
+
"Run renma scan.",
|
|
259
|
+
"Run renma catalog.",
|
|
260
|
+
"Confirm the next review date is not overdue.",
|
|
261
|
+
],
|
|
262
|
+
llmHint: `The next review date for ${entry.sourcePath} was ${dueAt}. Revalidate the asset before updating freshness metadata.`,
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
}
|
|
190
266
|
function duplicateAssetIdFindings(entries) {
|
|
191
267
|
const entriesById = new Map();
|
|
192
268
|
for (const entry of entries) {
|
|
@@ -197,7 +273,7 @@ function duplicateAssetIdFindings(entries) {
|
|
|
197
273
|
return [];
|
|
198
274
|
const paths = duplicates.map((entry) => entry.sourcePath).sort();
|
|
199
275
|
return duplicates.map((entry) => ({
|
|
200
|
-
id:
|
|
276
|
+
id: DIAGNOSTIC_IDS.META_DUPLICATE_ASSET_ID,
|
|
201
277
|
title: "Duplicate asset id",
|
|
202
278
|
category: "maintenance",
|
|
203
279
|
severity: "medium",
|
|
@@ -228,7 +304,7 @@ function unknownReferenceFindings(dependencies, resolver) {
|
|
|
228
304
|
return [];
|
|
229
305
|
return [
|
|
230
306
|
{
|
|
231
|
-
id:
|
|
307
|
+
id: DIAGNOSTIC_IDS.META_UNKNOWN_REFERENCE,
|
|
232
308
|
title: "Declared reference does not resolve to a known asset",
|
|
233
309
|
category: "maintenance",
|
|
234
310
|
severity: "medium",
|
|
@@ -264,7 +340,7 @@ function referenceDeprecatedAssetFindings(dependencies, resolver) {
|
|
|
264
340
|
}
|
|
265
341
|
return [
|
|
266
342
|
{
|
|
267
|
-
id:
|
|
343
|
+
id: DIAGNOSTIC_IDS.MAINT_REFERENCE_DEPRECATED_ASSET,
|
|
268
344
|
title: "Declared reference targets a deprecated or archived asset",
|
|
269
345
|
category: "maintenance",
|
|
270
346
|
severity: "medium",
|
|
@@ -311,7 +387,7 @@ function orphanedContextAssetFindings(entries, dependencies, resolver) {
|
|
|
311
387
|
return [];
|
|
312
388
|
return [
|
|
313
389
|
{
|
|
314
|
-
id:
|
|
390
|
+
id: DIAGNOSTIC_IDS.MAINT_ORPHANED_CONTEXT_ASSET,
|
|
315
391
|
title: "Shared context asset is not referenced by other assets",
|
|
316
392
|
category: "maintenance",
|
|
317
393
|
severity: "low",
|
|
@@ -392,10 +468,10 @@ export function severityMeets(value, threshold) {
|
|
|
392
468
|
function secretFindings(document) {
|
|
393
469
|
return matchingLineFindings(document, (line) => {
|
|
394
470
|
if (PRIVATE_KEY_PATTERN.test(line)) {
|
|
395
|
-
return finding(
|
|
471
|
+
return finding(DIAGNOSTIC_IDS.SEC_PRIVATE_KEY, "Private key material appears in repository text", "safety", "critical", document, "Remove the key, rotate it if real, and keep only setup instructions or placeholders.");
|
|
396
472
|
}
|
|
397
473
|
if (SECRET_PATTERN.test(line) && !isPlaceholder(line)) {
|
|
398
|
-
return finding(
|
|
474
|
+
return finding(DIAGNOSTIC_IDS.SEC_LITERAL_SECRET, "Literal credential-like value appears in repository text", "safety", "high", document, "Move secrets to user-approved inputs or a secret manager, and keep only placeholders in repository files.");
|
|
399
475
|
}
|
|
400
476
|
return undefined;
|
|
401
477
|
});
|
|
@@ -406,13 +482,13 @@ function commandFindings(document) {
|
|
|
406
482
|
return undefined;
|
|
407
483
|
if (DESTRUCTIVE_PATTERN.test(line) &&
|
|
408
484
|
!hasNearbyConfirmation(document.lines, line)) {
|
|
409
|
-
return finding(
|
|
485
|
+
return finding(DIAGNOSTIC_IDS.SEC_DESTRUCTIVE_COMMAND, "Dangerous command lacks explicit confirmation or recovery guard", "safety", "high", document, "Require explicit user confirmation, add dry-run/backup guidance, and describe rollback or verification.");
|
|
410
486
|
}
|
|
411
487
|
if (REMOTE_PATTERN.test(line)) {
|
|
412
|
-
return finding(
|
|
488
|
+
return finding(DIAGNOSTIC_IDS.SEC_REMOTE_DEFAULT, "Remote command example uses unsafe default", "safety", "medium", document, "Avoid production placeholders, insecure transport flags, and pipe-to-shell patterns unless paired with verification and confirmation.");
|
|
413
489
|
}
|
|
414
490
|
if (ENV_COPY_PATTERN.test(line)) {
|
|
415
|
-
return finding(
|
|
491
|
+
return finding(DIAGNOSTIC_IDS.SEC_ENV_COPY, "Command may pass broad environment into subprocess execution", "safety", "medium", document, "Pass only required environment variables to subprocesses and avoid forwarding secrets by default.");
|
|
416
492
|
}
|
|
417
493
|
return undefined;
|
|
418
494
|
});
|
|
@@ -425,17 +501,17 @@ function shapeFindings(document) {
|
|
|
425
501
|
const description = document.metadata.description ?? "";
|
|
426
502
|
const tokenCount = approximateTokenCount(document.artifact.content);
|
|
427
503
|
if (!description) {
|
|
428
|
-
findings.push(documentFinding(document,
|
|
504
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_DESCRIPTION, "Skill is missing an explicit description", "quality", "medium", "Add frontmatter description so agents can route to the skill intentionally."));
|
|
429
505
|
}
|
|
430
506
|
else if (document.artifact.kind === "skill" &&
|
|
431
507
|
description.length < DESCRIPTION_MIN_CHARS) {
|
|
432
|
-
findings.push(documentFinding(document,
|
|
508
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SHORT_DESCRIPTION, "Skill description is too short for routing clarity", "quality", "low", `Expand frontmatter description to at least ${DESCRIPTION_MIN_CHARS} characters with usage routing guidance.`));
|
|
433
509
|
}
|
|
434
510
|
const reusableContextFinding = reusableContextCandidateFinding(document, tokenCount);
|
|
435
511
|
if (reusableContextFinding)
|
|
436
512
|
findings.push(reusableContextFinding);
|
|
437
513
|
if (document.artifact.kind === "skill" && tokenCount > SKILL_TOKEN_LIMIT) {
|
|
438
|
-
findings.push(documentFinding(document,
|
|
514
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SKILL_TOKEN_BUDGET, "Skill entrypoint exceeds token budget", "quality", "medium", `Keep SKILL.md under about ${SKILL_TOKEN_LIMIT} tokens as a compact usage guide. Move detailed procedures into reference files, but preserve them losslessly in ordered parts when needed. Do not delete, summarize, or merge away procedural steps. SKILL.md should reference every required support file or index without embedding the full procedure.`, {
|
|
439
515
|
whyItMatters: "Large skills can mix LLM-facing usage guidance with reusable domain knowledge. Skills should remain concise routing contracts and usage guides, while reusable QA heuristics, domain rules, and tool guidance live in independently owned shared context assets.",
|
|
440
516
|
constraints: [
|
|
441
517
|
"Do not introduce runtime context resolution.",
|
|
@@ -454,23 +530,23 @@ function shapeFindings(document) {
|
|
|
454
530
|
}
|
|
455
531
|
if (document.artifact.kind === "skill" &&
|
|
456
532
|
USER_LOCAL_PATH_PATTERN.test(text)) {
|
|
457
|
-
findings.push(documentFinding(document,
|
|
533
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_USER_LOCAL_PATHS, "Skill uses hardcoded user home paths in instructions", "quality", "medium", "Use repo-relative or environment-agnostic paths in skill instructions. If a local path is unavoidable, parameterize it and avoid hardcoding a user-specific home directory such as `/Users/alice/...` or `/home/alice/...`."));
|
|
458
534
|
}
|
|
459
535
|
if (!/do not use for|non-goals|out of scope/.test(text)) {
|
|
460
|
-
findings.push(documentFinding(document,
|
|
536
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_NEGATIVE_ROUTING, "Skill lacks negative routing guidance", "structure", "medium", "Add a DO NOT USE FOR or non-goals section so agents know when to choose another path."));
|
|
461
537
|
}
|
|
462
538
|
if (!/use this skill|when to use|trigger|routing|context route|mixin/.test(text)) {
|
|
463
|
-
findings.push(documentFinding(document,
|
|
539
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_ROUTING_CLARITY, "Skill lacks routing clarity", "quality", "low", "Add concise routing language: when to use the skill, whether it invokes other skills, or whether it is a utility skill for single operations."));
|
|
464
540
|
}
|
|
465
541
|
if (!/example|input|output/.test(text)) {
|
|
466
|
-
findings.push(documentFinding(document,
|
|
542
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_EXAMPLES, "Skill lacks examples", "quality", "low", "Add examples that show representative inputs, outputs, or behavior."));
|
|
467
543
|
}
|
|
468
544
|
if (!/preflight|before you begin|first check|prerequisite|context/.test(text)) {
|
|
469
|
-
findings.push(documentFinding(document,
|
|
545
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_PREFLIGHT, "Skill lacks a preflight step", "quality", "medium", "Add a preflight section that captures environment, permissions, target files, and assumptions before acting."));
|
|
470
546
|
}
|
|
471
547
|
if (document.artifact.kind === "skill" &&
|
|
472
548
|
!REQUIRED_INPUTS_PATTERN.test(text)) {
|
|
473
|
-
findings.push(documentFinding(document,
|
|
549
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_REQUIRED_INPUTS, "Skill does not state required inputs", "quality", "medium", "Add a Required inputs or Prerequisites section that states the user-provided inputs, target files, repository state, permissions, credentials, or environment assumptions needed before the workflow can start.", {
|
|
474
550
|
whyItMatters: "Agents need explicit input requirements before starting a workflow. Missing required inputs can cause the agent to guess targets, assume permissions, or start without enough repository context.",
|
|
475
551
|
constraints: [
|
|
476
552
|
"Do not infer runtime context.",
|
|
@@ -489,7 +565,7 @@ function shapeFindings(document) {
|
|
|
489
565
|
}
|
|
490
566
|
if (document.artifact.kind === "skill" &&
|
|
491
567
|
!COMPLETION_CRITERIA_PATTERN.test(text)) {
|
|
492
|
-
findings.push(documentFinding(document,
|
|
568
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_COMPLETION_CRITERIA, "Skill does not state completion criteria", "quality", "medium", "Add a Completion criteria, Success requirements, Deliverables, or Final response section that states the observable outputs or conditions that mean the workflow is complete.", {
|
|
493
569
|
whyItMatters: "Agents need explicit completion criteria before finishing a workflow. Missing completion criteria can cause incomplete delivery, unnecessary follow-up work, or inconsistent final responses.",
|
|
494
570
|
constraints: [
|
|
495
571
|
"Do not infer runtime context.",
|
|
@@ -507,11 +583,11 @@ function shapeFindings(document) {
|
|
|
507
583
|
}));
|
|
508
584
|
}
|
|
509
585
|
if (!/verify|validation|test|confirm result|expected output/.test(text)) {
|
|
510
|
-
findings.push(documentFinding(document,
|
|
586
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_VERIFICATION, "Skill lacks verification guidance", "quality", "medium", "State how to verify success with a command, check, or observable result."));
|
|
511
587
|
}
|
|
512
588
|
if (document.headings.length < 2 &&
|
|
513
589
|
document.artifact.content.split(/\s+/).length > 120) {
|
|
514
|
-
findings.push(documentFinding(document,
|
|
590
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_LOW_HEADING_DENSITY, "Long instruction file has few headings", "structure", "low", "Split long prose into task-oriented headings so agents can navigate it reliably."));
|
|
515
591
|
}
|
|
516
592
|
return findings;
|
|
517
593
|
}
|
|
@@ -561,7 +637,7 @@ function reusableContextCandidateFinding(document, tokenCount) {
|
|
|
561
637
|
: undefined,
|
|
562
638
|
].filter((part) => Boolean(part));
|
|
563
639
|
return {
|
|
564
|
-
id:
|
|
640
|
+
id: DIAGNOSTIC_IDS.MAINT_SKILL_REUSABLE_CONTEXT_CANDIDATE,
|
|
565
641
|
title: "Skill may contain reusable context worth extracting",
|
|
566
642
|
category: "maintenance",
|
|
567
643
|
severity: "low",
|
|
@@ -647,7 +723,7 @@ function supportSharedContextCandidateFindings(document) {
|
|
|
647
723
|
].join("\n");
|
|
648
724
|
return [
|
|
649
725
|
{
|
|
650
|
-
id:
|
|
726
|
+
id: DIAGNOSTIC_IDS.MAINT_SUPPORT_ASSET_SHARED_CONTEXT_CANDIDATE,
|
|
651
727
|
title: "Skill-local support file may be a shared context candidate",
|
|
652
728
|
category: "maintenance",
|
|
653
729
|
severity: "low",
|
|
@@ -688,7 +764,7 @@ function contextPathNonSemanticFindings(document) {
|
|
|
688
764
|
return [];
|
|
689
765
|
return [
|
|
690
766
|
{
|
|
691
|
-
id:
|
|
767
|
+
id: DIAGNOSTIC_IDS.MAINT_CONTEXT_PATH_NON_SEMANTIC,
|
|
692
768
|
title: "Context asset path appears process-oriented rather than semantic",
|
|
693
769
|
category: "maintenance",
|
|
694
770
|
severity: "low",
|
|
@@ -736,7 +812,7 @@ function skillContextReferenceNotDeclaredFindings(document) {
|
|
|
736
812
|
return [...matches.entries()]
|
|
737
813
|
.filter(([referencedPath]) => !declaredContexts.has(referencedPath))
|
|
738
814
|
.map(([referencedPath, match]) => ({
|
|
739
|
-
id:
|
|
815
|
+
id: DIAGNOSTIC_IDS.MAINT_SKILL_CONTEXT_REFERENCE_NOT_DECLARED,
|
|
740
816
|
title: "Skill references a shared context without declaring it",
|
|
741
817
|
category: "maintenance",
|
|
742
818
|
severity: "low",
|
|
@@ -793,7 +869,7 @@ function skillReferencesSupersededAssetFindings(documents) {
|
|
|
793
869
|
].join("\n");
|
|
794
870
|
return [
|
|
795
871
|
{
|
|
796
|
-
id:
|
|
872
|
+
id: DIAGNOSTIC_IDS.MAINT_SKILL_REFERENCES_SUPERSEDED_ASSET,
|
|
797
873
|
title: "Skill references a superseded local support asset",
|
|
798
874
|
category: "maintenance",
|
|
799
875
|
severity: "low",
|
|
@@ -881,7 +957,7 @@ function assetReferencesSupersededAssetFindings(documents) {
|
|
|
881
957
|
].join("\n");
|
|
882
958
|
return [
|
|
883
959
|
{
|
|
884
|
-
id:
|
|
960
|
+
id: DIAGNOSTIC_IDS.MAINT_ASSET_REFERENCES_SUPERSEDED_ASSET,
|
|
885
961
|
title: "Asset references a superseded support file",
|
|
886
962
|
category: "maintenance",
|
|
887
963
|
severity: "low",
|
|
@@ -951,7 +1027,7 @@ function contextBudgetFindings(document) {
|
|
|
951
1027
|
if (tokenCount <= limit)
|
|
952
1028
|
return [];
|
|
953
1029
|
return [
|
|
954
|
-
documentFinding(document,
|
|
1030
|
+
documentFinding(document, DIAGNOSTIC_IDS.QUAL_SUPPORT_ASSET_TOKEN_BUDGET, "Support asset exceeds token guidance", "quality", "low", `Keep ${document.artifact.kind} assets under about ${limit} tokens where practical. If a file is too large, run \`renma suggest-semantic-split ${document.artifact.path}\` to get a semantic split proposal, then split it losslessly into meaning-based ordered part files. Do not delete, summarize, or merge away procedural steps. The parent file or SKILL.md should reference every part in order, and the split should preserve the original procedure text exactly. Verify by reconstructing the parts and comparing them to the original content before accepting the fix.`, {
|
|
955
1031
|
whyItMatters: "Oversized support assets are harder for humans and LLM coding agents to review safely. Shared context and local support files should stay modular enough that ownership, scope, and static references remain clear.",
|
|
956
1032
|
constraints: [
|
|
957
1033
|
"Do not introduce runtime context resolution.",
|
|
@@ -975,7 +1051,7 @@ function profileFindings(document) {
|
|
|
975
1051
|
if (/base[_ -]?skill|extends/.test(text))
|
|
976
1052
|
return [];
|
|
977
1053
|
return [
|
|
978
|
-
documentFinding(document,
|
|
1054
|
+
documentFinding(document, DIAGNOSTIC_IDS.PROF_MISSING_BASE, "Profile overlay does not declare its base skill", "structure", "medium", "Declare the base skill or compatibility target so routing conflicts are auditable."),
|
|
979
1055
|
];
|
|
980
1056
|
}
|
|
981
1057
|
function skillLocalSupportReachabilityFindings(documents) {
|
|
@@ -990,7 +1066,7 @@ function skillLocalSupportReachabilityFindings(documents) {
|
|
|
990
1066
|
const text = skill.artifact.content.toLowerCase();
|
|
991
1067
|
const hasLocalSupportGuidance = /support file|local support|context route|context map|mixin|profiles?\/|references?\/|examples?\/|load .*?(?:profile|reference|example)|reference .*?(?:profile|reference|example)/.test(text);
|
|
992
1068
|
if (!hasLocalSupportGuidance) {
|
|
993
|
-
findings.push(documentFinding(skill,
|
|
1069
|
+
findings.push(documentFinding(skill, DIAGNOSTIC_IDS.SUPPORT_MISSING_REACHABILITY_GUIDANCE, "Skill has local support files but no reachability guidance", "structure", "medium", "Add local support file reachability guidance so the top-level skill declares when profiles, references, examples, or scripts are reachable. If support content was split into ordered parts, reference the index or all parts in order. Preserve original concrete steps. Do not delete, summarize, or merge away procedural steps.", {
|
|
994
1070
|
whyItMatters: "Local support files should be statically discoverable from the skill so humans and LLM coding agents can tell which repository evidence belongs to the skill without relying on runtime context selection.",
|
|
995
1071
|
constraints: [
|
|
996
1072
|
"Do not introduce runtime context resolution.",
|
|
@@ -1117,7 +1193,7 @@ function disallowedSkillAssetFindings(document, config) {
|
|
|
1117
1193
|
const [, skillName = "", assetRoot = "", rest = ""] = match;
|
|
1118
1194
|
const target = canonicalSkillAssetTarget(config, skillName, assetRoot, rest);
|
|
1119
1195
|
return [
|
|
1120
|
-
documentFinding(document,
|
|
1196
|
+
documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_DISALLOWED_SKILL_ASSET, `Disallowed skill-local ${assetRoot} asset`, "structure", assetRoot === "scripts" ? "medium" : "low", `Move this file to \`${target}\` and update every repo-local reference to the new canonical path.`, {
|
|
1121
1197
|
whyItMatters: "Strict layout keeps skills as thin entrypoints, shared context under contexts/, and executable helpers under tools/ so agents can migrate repositories deterministically.",
|
|
1122
1198
|
verificationSteps: [
|
|
1123
1199
|
`Move ${document.artifact.path} to ${target}.`,
|
|
@@ -1147,7 +1223,7 @@ function thinSkillLayoutFindings(document) {
|
|
|
1147
1223
|
];
|
|
1148
1224
|
if (wordCount > 450 ||
|
|
1149
1225
|
procedureSignals.some((pattern) => pattern.test(document.artifact.content))) {
|
|
1150
|
-
findings.push(documentFinding(document,
|
|
1226
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_NOT_THIN, "Skill entrypoint contains procedure content", "structure", wordCount > 700 ? "medium" : "low", "Move reusable procedure content into contexts/** and keep SKILL.md as a concise router that points to the required context assets.", {
|
|
1151
1227
|
whyItMatters: "Strict layout expects skills/*/SKILL.md to route agents, not own canonical references, profiles, examples, scripts, or detailed procedures.",
|
|
1152
1228
|
verificationSteps: [
|
|
1153
1229
|
"Confirm SKILL.md contains routing guidance and required context references only.",
|
|
@@ -1158,7 +1234,7 @@ function thinSkillLayoutFindings(document) {
|
|
|
1158
1234
|
}
|
|
1159
1235
|
const command = firstExecutableCommand(document);
|
|
1160
1236
|
if (command) {
|
|
1161
|
-
findings.push(findingAt(document,
|
|
1237
|
+
findings.push(findingAt(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_EXECUTABLE_COMMAND, "Skill entrypoint contains executable command", "structure", "low", command.line, command.command, "Move executable setup commands into contexts/** procedures or tools/** helper documentation, and keep SKILL.md as a router.", {
|
|
1162
1238
|
whyItMatters: "Executable setup commands in SKILL.md make the entrypoint procedural and harder to migrate to the strict three-root layout.",
|
|
1163
1239
|
verificationSteps: [
|
|
1164
1240
|
"Move the command guidance to a context/reference/procedure.",
|
|
@@ -1175,7 +1251,7 @@ function helperCommandFindings(document, root, paths, config) {
|
|
|
1175
1251
|
if (!scriptPath)
|
|
1176
1252
|
continue;
|
|
1177
1253
|
if (/^skills\/[^/]+\/scripts\//.test(scriptPath)) {
|
|
1178
|
-
findings.push(findingAt(document,
|
|
1254
|
+
findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_SKILL_SCRIPTS, "Helper command points to skill-local scripts", "structure", "medium", command.line, command.command, `Move the helper script to \`${canonicalHelperTarget(config, scriptPath)}\` and update this command to use the tools/** path.`, {
|
|
1179
1255
|
whyItMatters: "Strict layout keeps executable helper assets under tools/**, not under skills/**.",
|
|
1180
1256
|
verificationSteps: [
|
|
1181
1257
|
`Confirm ${canonicalHelperTarget(config, scriptPath)} exists.`,
|
|
@@ -1185,7 +1261,7 @@ function helperCommandFindings(document, root, paths, config) {
|
|
|
1185
1261
|
continue;
|
|
1186
1262
|
}
|
|
1187
1263
|
if (scriptPath.includes("/scripts/") && !scriptPath.startsWith("tools/")) {
|
|
1188
|
-
findings.push(findingAt(document,
|
|
1264
|
+
findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_NON_TOOLS, "Helper command does not use tools root", "structure", "low", command.line, command.command, "Update helper script commands to reference scripts under tools/**.", {
|
|
1189
1265
|
whyItMatters: "Helper commands should resolve to non-context helper assets in tools/** so contexts remain LLM-readable guidance.",
|
|
1190
1266
|
}));
|
|
1191
1267
|
continue;
|
|
@@ -1193,7 +1269,7 @@ function helperCommandFindings(document, root, paths, config) {
|
|
|
1193
1269
|
if (scriptPath.startsWith("tools/") &&
|
|
1194
1270
|
!paths.has(scriptPath) &&
|
|
1195
1271
|
!(root && existsSync(path.join(root, scriptPath)))) {
|
|
1196
|
-
findings.push(findingAt(document,
|
|
1272
|
+
findings.push(findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_UNRESOLVED, "Helper command target does not resolve", "structure", "medium", command.line, command.command, `Create \`${scriptPath}\` or update this command to the correct tools/** helper path.`, {
|
|
1197
1273
|
whyItMatters: "Agents need helper commands in markdown procedures to resolve deterministically before running them.",
|
|
1198
1274
|
}));
|
|
1199
1275
|
}
|
|
@@ -1229,7 +1305,7 @@ function layoutConsistencyFindings(document) {
|
|
|
1229
1305
|
if (!match)
|
|
1230
1306
|
continue;
|
|
1231
1307
|
const line = lineForOffset(text, match.index ?? 0);
|
|
1232
|
-
findings.push(findingAt(document,
|
|
1308
|
+
findings.push(findingAt(document, DIAGNOSTIC_IDS.DOCS_LAYOUT_INCONSISTENT, "Repository docs describe a non-canonical layout", "maintenance", "low", line, lineText(document, line), stale.message, {
|
|
1233
1309
|
whyItMatters: "README.md and AGENTS.md should teach agents the same strict three-root layout that Renma enforces.",
|
|
1234
1310
|
verificationSteps: [
|
|
1235
1311
|
"Update README.md and AGENTS.md to mention skills/, contexts/, and tools/ canonical roots.",
|
|
@@ -1242,7 +1318,7 @@ function layoutConsistencyFindings(document) {
|
|
|
1242
1318
|
function contextRootFindings(document) {
|
|
1243
1319
|
if (document.artifact.path.startsWith("context/")) {
|
|
1244
1320
|
return [
|
|
1245
|
-
documentFinding(document,
|
|
1321
|
+
documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_CONTEXT_LEGACY_ROOT, "Context asset uses legacy context/ root", "structure", "low", "Move canonical LLM-readable context assets under contexts/**.", {
|
|
1246
1322
|
whyItMatters: "The strict repository layout uses contexts/**/*.md as canonical LLM-readable context assets.",
|
|
1247
1323
|
}),
|
|
1248
1324
|
];
|
|
@@ -1253,7 +1329,7 @@ function helperRootFindings(document) {
|
|
|
1253
1329
|
if (document.artifact.path.includes("/scripts/") &&
|
|
1254
1330
|
!document.artifact.path.startsWith("tools/")) {
|
|
1255
1331
|
return [
|
|
1256
|
-
documentFinding(document,
|
|
1332
|
+
documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_HELPER_NON_TOOLS, "Helper script is outside tools root", "structure", "medium", "Move non-context helper assets under tools/** and update command references.", {
|
|
1257
1333
|
whyItMatters: "The strict repository layout reserves tools/** for executable and non-context helper assets.",
|
|
1258
1334
|
}),
|
|
1259
1335
|
];
|
|
@@ -1278,7 +1354,7 @@ function declaredDependencyLayoutFindings(catalog, paths, root) {
|
|
|
1278
1354
|
if (!source)
|
|
1279
1355
|
continue;
|
|
1280
1356
|
findings.push({
|
|
1281
|
-
id:
|
|
1357
|
+
id: DIAGNOSTIC_IDS.LAYOUT_CONTEXT_REFERENCE_NON_CANONICAL,
|
|
1282
1358
|
title: "Declared context path is not under canonical roots",
|
|
1283
1359
|
category: "structure",
|
|
1284
1360
|
severity: "low",
|
|
@@ -1401,10 +1477,10 @@ function evidence(document, line, snippet) {
|
|
|
1401
1477
|
}
|
|
1402
1478
|
function localSupportUnreachableRuleId(kind) {
|
|
1403
1479
|
if (kind === "profile")
|
|
1404
|
-
return
|
|
1480
|
+
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_PROFILE;
|
|
1405
1481
|
if (kind === "example")
|
|
1406
|
-
return
|
|
1407
|
-
return
|
|
1482
|
+
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_EXAMPLE;
|
|
1483
|
+
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_REFERENCE;
|
|
1408
1484
|
}
|
|
1409
1485
|
function escapeRegExp(value) {
|
|
1410
1486
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|