renma 0.1.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +350 -0
  3. package/dist/catalog.d.ts +8 -0
  4. package/dist/catalog.d.ts.map +1 -0
  5. package/dist/catalog.js +140 -0
  6. package/dist/catalog.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +301 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/catalog.d.ts +24 -0
  12. package/dist/commands/catalog.d.ts.map +1 -0
  13. package/dist/commands/catalog.js +88 -0
  14. package/dist/commands/catalog.js.map +1 -0
  15. package/dist/commands/graph.d.ts +45 -0
  16. package/dist/commands/graph.d.ts.map +1 -0
  17. package/dist/commands/graph.js +344 -0
  18. package/dist/commands/graph.js.map +1 -0
  19. package/dist/commands/inspect.d.ts +36 -0
  20. package/dist/commands/inspect.d.ts.map +1 -0
  21. package/dist/commands/inspect.js +143 -0
  22. package/dist/commands/inspect.js.map +1 -0
  23. package/dist/commands/ownership.d.ts +50 -0
  24. package/dist/commands/ownership.d.ts.map +1 -0
  25. package/dist/commands/ownership.js +154 -0
  26. package/dist/commands/ownership.js.map +1 -0
  27. package/dist/commands/readiness.d.ts +64 -0
  28. package/dist/commands/readiness.d.ts.map +1 -0
  29. package/dist/commands/readiness.js +614 -0
  30. package/dist/commands/readiness.js.map +1 -0
  31. package/dist/commands/scan.d.ts +4 -0
  32. package/dist/commands/scan.d.ts.map +1 -0
  33. package/dist/commands/scan.js +12 -0
  34. package/dist/commands/scan.js.map +1 -0
  35. package/dist/commands/suggest-semantic-split.d.ts +8 -0
  36. package/dist/commands/suggest-semantic-split.d.ts.map +1 -0
  37. package/dist/commands/suggest-semantic-split.js +215 -0
  38. package/dist/commands/suggest-semantic-split.js.map +1 -0
  39. package/dist/config.d.ts +15 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +184 -0
  42. package/dist/config.js.map +1 -0
  43. package/dist/discovery.d.ts +7 -0
  44. package/dist/discovery.d.ts.map +1 -0
  45. package/dist/discovery.js +122 -0
  46. package/dist/discovery.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +4 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/markdown.d.ts +4 -0
  52. package/dist/markdown.d.ts.map +1 -0
  53. package/dist/markdown.js +77 -0
  54. package/dist/markdown.js.map +1 -0
  55. package/dist/metadata.d.ts +8 -0
  56. package/dist/metadata.d.ts.map +1 -0
  57. package/dist/metadata.js +61 -0
  58. package/dist/metadata.js.map +1 -0
  59. package/dist/model.d.ts +56 -0
  60. package/dist/model.d.ts.map +1 -0
  61. package/dist/model.js +2 -0
  62. package/dist/model.js.map +1 -0
  63. package/dist/report.d.ts +6 -0
  64. package/dist/report.d.ts.map +1 -0
  65. package/dist/report.js +39 -0
  66. package/dist/report.js.map +1 -0
  67. package/dist/rule-engine.d.ts +16 -0
  68. package/dist/rule-engine.d.ts.map +1 -0
  69. package/dist/rule-engine.js +10 -0
  70. package/dist/rule-engine.js.map +1 -0
  71. package/dist/rules.d.ts +7 -0
  72. package/dist/rules.d.ts.map +1 -0
  73. package/dist/rules.js +1413 -0
  74. package/dist/rules.js.map +1 -0
  75. package/dist/scanner.d.ts +5 -0
  76. package/dist/scanner.d.ts.map +1 -0
  77. package/dist/scanner.js +104 -0
  78. package/dist/scanner.js.map +1 -0
  79. package/dist/types.d.ts +99 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/package.json +56 -0
  84. package/scripts/split-reference.mjs +172 -0
package/dist/rules.js ADDED
@@ -0,0 +1,1413 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { runRuleRegistry } from "./rule-engine.js";
4
+ const SECRET_PATTERN = /\b(?:password|passwd|token|api[_-]?key|secret|credential|private[_-]?key)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{8,})/i;
5
+ const PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/;
6
+ const DESTRUCTIVE_PATTERN = /\b(?:rm\s+-rf|mkfs|dd\s+if=|chmod\s+-R\s+777|chown\s+-R|sudo\s+(?:rm|dd|mkfs|chmod|chown)|git\s+clean\s+-fdx|docker\s+system\s+prune)\b/i;
7
+ const REMOTE_PATTERN = /\b(?:curl|wget)\b.*(?:\|\s*(?:sh|bash)|\b(?:example\.com|prod|production|--insecure|-k)\b)|\b(?:ssh|scp)\b.*\b(?:example\.com|prod|production|root@|--insecure|-k|StrictHostKeyChecking=no|UserKnownHostsFile=\/dev\/null)\b/i;
8
+ const ENV_COPY_PATTERN = /\b(?:process\.env|env)\b.*\b(?:spawn|exec|execFile|system|subprocess|child_process)\b|\b(?:spawn|exec|execFile|system|subprocess|child_process)\b.*\b(?:process\.env|env)\b/i;
9
+ const USER_LOCAL_PATH_PATTERN = /(?:^|[^a-z0-9_])(?:\/Users\/[^\s/\\]+|\/home\/[^\s/\\]+|[A-Za-z]:\\Users\\[^\s\\]+)(?:\/|$)/iu;
10
+ const SKILL_TOKEN_LIMIT = 500;
11
+ const DESCRIPTION_MIN_CHARS = 150;
12
+ const REUSABLE_CONTEXT_MIN_LINES = 24;
13
+ const REUSABLE_CONTEXT_MIN_TOKENS = 180;
14
+ const REQUIRED_INPUTS_PATTERN = /\b(?:required inputs?|inputs|input requirements?|required information|prerequisites?|required context|required files|required permissions?|permission requirements?|environment requirements?|before running,\s*provide|before you begin,\s*provide|the user must provide|needs the following|target files|permissions required|environment required)\b|\brequires:/;
15
+ const COMPLETION_CRITERIA_PATTERN = /\b(?:completion criteria|completion checklist|success criteria|success requirements|done criteria|done when|definition of done|acceptance criteria|deliverables?|final response|final answer|expected outcomes?|expected results?|expected output|required output|output requirements?|report should include|patch should include|when complete|workflow is complete|the workflow is complete after|task is complete|counts as complete|completion requirements?|stop when|do not finish until)\b/;
16
+ const REUSABLE_CONTEXT_MIN_SIGNALS = 3;
17
+ const SUPPORT_SHARED_CONTEXT_MIN_LINES = 18;
18
+ const SUPPORT_SHARED_CONTEXT_MIN_TOKENS = 140;
19
+ const SUPPORT_SHARED_CONTEXT_MIN_HEADINGS = 2;
20
+ const SUPPORT_SHARED_CONTEXT_MIN_PHRASES = 2;
21
+ const REUSABLE_CONTEXT_HEADING_PATTERNS = [
22
+ [/\bsetup\b/i, "Setup"],
23
+ [/\binstallation\b/i, "Installation"],
24
+ [/\bconfiguration\b/i, "Configuration"],
25
+ [/\benvironment\b/i, "Environment"],
26
+ [/\bprerequisites?\b/i, "Prerequisites"],
27
+ [/\bplatform\b/i, "Platform"],
28
+ [/\btroubleshooting\b/i, "Troubleshooting"],
29
+ [/\bknown issues?\b/i, "Known Issues"],
30
+ [/\blimitations?\b/i, "Limitations"],
31
+ [/\bbest practices?\b/i, "Best Practices"],
32
+ [/\btesting heuristics?\b/i, "Testing Heuristics"],
33
+ [/\btest strategy\b/i, "Test Strategy"],
34
+ [/\bverification\b/i, "Verification"],
35
+ [/\bexamples?\b/i, "Examples"],
36
+ [/\bedge cases?\b/i, "Edge Cases"],
37
+ [/\brisks?\b/i, "Risks"],
38
+ [/\bdomain rules?\b/i, "Domain Rules"],
39
+ [/\bfailure modes?\b/i, "Failure Modes"],
40
+ [/\bflaky tests?\b/i, "Flaky Tests"],
41
+ ];
42
+ const REUSABLE_CONTEXT_PHRASE_PATTERNS = [
43
+ [/\buse this when\b/i, "use this when"],
44
+ [/\bknown issue\b/i, "known issue"],
45
+ [/\blimitation\b/i, "limitation"],
46
+ [/\btroubleshooting\b/i, "troubleshooting"],
47
+ [/\bflaky\b/i, "flaky"],
48
+ [/\bretry\b/i, "retry"],
49
+ [/\bplatform-specific\b/i, "platform-specific"],
50
+ [/\bedge case\b/i, "edge case"],
51
+ [/\brisk\b/i, "risk"],
52
+ [/\bheuristic\b/i, "heuristic"],
53
+ [/\bbest practice\b/i, "best practice"],
54
+ [/\bdo not\b/i, "do not"],
55
+ [/\bavoid\b/i, "avoid"],
56
+ [/\balways\b/i, "always"],
57
+ [/\bnever\b/i, "never"],
58
+ ];
59
+ const SUPPORT_SHARED_CONTEXT_HEADING_PATTERNS = [
60
+ ...REUSABLE_CONTEXT_HEADING_PATTERNS,
61
+ [/\bdecision logic\b/i, "Decision Logic"],
62
+ [/\bsafety notes?\b/i, "Safety Notes"],
63
+ [/\bvalidation\b/i, "Validation"],
64
+ [/\boperating model\b/i, "Operating Model"],
65
+ [/\bpolicy\b/i, "Policy"],
66
+ [/\bguidelines?\b/i, "Guidelines"],
67
+ [/\bprocedures?\b/i, "Procedure"],
68
+ [/\bchecklist\b/i, "Checklist"],
69
+ [/\bcompatibility\b/i, "Compatibility"],
70
+ [/\bconstraints?\b/i, "Constraints"],
71
+ ];
72
+ const SUPPORT_SHARED_CONTEXT_PHRASE_PATTERNS = [
73
+ [/\bmust\b/i, "must"],
74
+ [/\bshould\b/i, "should"],
75
+ [/\balways\b/i, "always"],
76
+ [/\bnever\b/i, "never"],
77
+ [/\bavoid\b/i, "avoid"],
78
+ [/\bprefer\b/i, "prefer"],
79
+ [/\bdo not\b/i, "do not"],
80
+ [/\brequired\b/i, "required"],
81
+ [/\brecommended\b/i, "recommended"],
82
+ [/\bknown issue\b/i, "known issue"],
83
+ [/\blimitation\b/i, "limitation"],
84
+ [/\bfailure mode\b/i, "failure mode"],
85
+ [/\btroubleshooting\b/i, "troubleshooting"],
86
+ [/\bedge case\b/i, "edge case"],
87
+ [/\brisk\b/i, "risk"],
88
+ [/\bbest practice\b/i, "best practice"],
89
+ [/\bvalidate\b/i, "validate"],
90
+ [/\bverify\b/i, "verify"],
91
+ ];
92
+ const NON_SEMANTIC_CONTEXT_PATH_SEGMENTS = new Set([
93
+ "promoted",
94
+ "generated",
95
+ "split",
96
+ "migrated",
97
+ "migration",
98
+ "new",
99
+ "old",
100
+ "tmp",
101
+ "temp",
102
+ "draft",
103
+ "drafts",
104
+ "wip",
105
+ "misc",
106
+ "miscellaneous",
107
+ "todo",
108
+ "review",
109
+ "staging",
110
+ "candidate",
111
+ "candidates",
112
+ ]);
113
+ const CONTEXT_TOKEN_LIMITS = {
114
+ context: 1200,
115
+ profile: 500,
116
+ reference: 800,
117
+ example: 800,
118
+ };
119
+ /** Run all deterministic rules and return findings in stable source order. */
120
+ export function runRules(documents, config, catalog) {
121
+ const findings = runRuleRegistry(documents, RULES, catalog, config);
122
+ return findings.sort((a, b) => {
123
+ const byPath = a.evidence.path.localeCompare(b.evidence.path);
124
+ if (byPath !== 0)
125
+ return byPath;
126
+ return a.evidence.startLine - b.evidence.startLine;
127
+ });
128
+ }
129
+ const RULES = [
130
+ {
131
+ id: "strict-layout-policy",
132
+ run: (context) => strictLayoutPolicyFindings(context.documents, context.config, context.catalog),
133
+ },
134
+ {
135
+ id: "security",
136
+ run: ({ documents }) => documents.flatMap((document) => [
137
+ ...secretFindings(document),
138
+ ...commandFindings(document),
139
+ ]),
140
+ },
141
+ {
142
+ id: "shape",
143
+ run: ({ documents }) => documents.flatMap((document) => [
144
+ ...shapeFindings(document),
145
+ ...contextBudgetFindings(document),
146
+ ...profileFindings(document),
147
+ ]),
148
+ },
149
+ {
150
+ id: "skill-local-support-reachability",
151
+ run: ({ documents }) => skillLocalSupportReachabilityFindings(documents),
152
+ },
153
+ {
154
+ id: "support-asset-shared-context-candidate",
155
+ run: ({ documents }) => documents.flatMap((document) => supportSharedContextCandidateFindings(document)),
156
+ },
157
+ {
158
+ id: "context-path-non-semantic",
159
+ run: ({ documents }) => documents.flatMap((document) => contextPathNonSemanticFindings(document)),
160
+ },
161
+ {
162
+ id: "skill-context-reference-not-declared",
163
+ run: ({ documents }) => documents.flatMap((document) => skillContextReferenceNotDeclaredFindings(document)),
164
+ },
165
+ {
166
+ id: "skill-references-superseded-asset",
167
+ run: ({ documents }) => skillReferencesSupersededAssetFindings(documents),
168
+ },
169
+ {
170
+ id: "asset-references-superseded-asset",
171
+ run: ({ documents }) => assetReferencesSupersededAssetFindings(documents),
172
+ },
173
+ {
174
+ id: "catalog-declared-reference-governance",
175
+ run: ({ catalog }) => catalogDeclaredReferenceGovernanceFindings(catalog),
176
+ },
177
+ ];
178
+ function catalogDeclaredReferenceGovernanceFindings(catalog) {
179
+ if (!catalog)
180
+ return [];
181
+ const resolver = createCatalogReferenceResolver(catalog.entries);
182
+ const findings = [
183
+ ...duplicateAssetIdFindings(catalog.entries),
184
+ ...unknownReferenceFindings(catalog.dependencies, resolver),
185
+ ...referenceDeprecatedAssetFindings(catalog.dependencies, resolver),
186
+ ...orphanedContextAssetFindings(catalog.entries, catalog.dependencies, resolver),
187
+ ];
188
+ return findings;
189
+ }
190
+ function duplicateAssetIdFindings(entries) {
191
+ const entriesById = new Map();
192
+ for (const entry of entries) {
193
+ entriesById.set(entry.id, [...(entriesById.get(entry.id) ?? []), entry]);
194
+ }
195
+ return [...entriesById.entries()].flatMap(([assetId, duplicates]) => {
196
+ if (duplicates.length < 2)
197
+ return [];
198
+ const paths = duplicates.map((entry) => entry.sourcePath).sort();
199
+ return duplicates.map((entry) => ({
200
+ id: "META-DUPLICATE-ASSET-ID",
201
+ title: "Duplicate asset id",
202
+ category: "maintenance",
203
+ severity: "medium",
204
+ confidence: "high",
205
+ evidence: metadataFindingEvidence(entry.sourcePath, `Duplicate asset id: ${assetId}`),
206
+ whyItMatters: "Asset ids make skills, contexts, and support assets referenceable across the repository. Duplicate ids make dependency validation, ownership, and agent-readable cataloging ambiguous.",
207
+ remediation: "Give each asset a unique stable id. If the assets represent the same source-of-truth knowledge, merge or deprecate one of them. If they are distinct, rename one id to reflect its actual scope.",
208
+ constraints: [
209
+ "Do not introduce runtime context resolution.",
210
+ "Do not create prompt packages.",
211
+ "Do not make Renma call an LLM.",
212
+ "Do not automatically rewrite ids during scan.",
213
+ "Update declared references through a reviewable patch after renaming an id.",
214
+ ],
215
+ verificationSteps: [
216
+ "Run renma scan.",
217
+ "Run renma catalog.",
218
+ "Run any project-specific validation checks that apply to this repository.",
219
+ "Confirm each asset id is unique and references still point to the intended asset.",
220
+ ],
221
+ llmHint: `Find all assets with id "${assetId}", compare their scope and metadata, and propose a merge/deprecation path or unique replacement ids. Duplicate paths: ${paths.join(", ")}`,
222
+ }));
223
+ });
224
+ }
225
+ function unknownReferenceFindings(dependencies, resolver) {
226
+ return dependencies.flatMap((dependency) => {
227
+ if (resolver.resolve(dependency.to))
228
+ return [];
229
+ return [
230
+ {
231
+ id: "META-UNKNOWN-REFERENCE",
232
+ title: "Declared reference does not resolve to a known asset",
233
+ category: "maintenance",
234
+ severity: "medium",
235
+ confidence: "high",
236
+ evidence: dependency.evidence ??
237
+ metadataFindingEvidence(dependency.sourcePath, `Unresolved ${dependency.kind} reference: ${dependency.to}`),
238
+ whyItMatters: "Declared references make repository relationships visible to catalog, graph, and validation reports. Unknown references make skills and context assets harder for humans and agents to trust.",
239
+ remediation: "Fix the reference so it points to an existing asset id or repository-relative path, or remove it if the relationship is no longer needed.",
240
+ constraints: [
241
+ "Do not select runtime context.",
242
+ "Do not assemble prompt packages.",
243
+ "Do not infer missing dependencies with an LLM during scan.",
244
+ "Only validate declared repository relationships.",
245
+ ],
246
+ verificationSteps: [
247
+ "Run renma scan.",
248
+ "Run renma catalog.",
249
+ "Confirm declared references resolve to known assets.",
250
+ ],
251
+ llmHint: `Search the repository for the intended asset by nearby filename, title, id, or path. Update or remove unresolved ${dependency.kind} reference "${dependency.to}" declared by "${dependency.from}".`,
252
+ },
253
+ ];
254
+ });
255
+ }
256
+ function referenceDeprecatedAssetFindings(dependencies, resolver) {
257
+ return dependencies.flatMap((dependency) => {
258
+ const target = resolver.resolve(dependency.to);
259
+ if (!target)
260
+ return [];
261
+ if (target.metadata.status !== "deprecated" &&
262
+ target.metadata.status !== "archived") {
263
+ return [];
264
+ }
265
+ return [
266
+ {
267
+ id: "MAINT-REFERENCE-DEPRECATED-ASSET",
268
+ title: "Declared reference targets a deprecated or archived asset",
269
+ category: "maintenance",
270
+ severity: "medium",
271
+ confidence: "high",
272
+ evidence: dependency.evidence ??
273
+ metadataFindingEvidence(dependency.sourcePath, `Reference to ${target.metadata.status} asset: ${dependency.to}`),
274
+ whyItMatters: "Declared references to deprecated or archived assets can keep old knowledge in active repository paths. If a canonical replacement exists, assets should usually reference that replacement directly.",
275
+ remediation: "Update the declared reference to point to the canonical replacement if one exists, or document why the deprecated or archived asset is still intentionally referenced.",
276
+ constraints: [
277
+ "Do not introduce runtime context resolution.",
278
+ "Do not create prompt packages.",
279
+ "Do not automatically rewrite references during scan.",
280
+ "Preserve compatibility shims when they are intentionally needed.",
281
+ ],
282
+ verificationSteps: [
283
+ "Run renma scan.",
284
+ "Run renma catalog.",
285
+ "Confirm active assets do not declare dependencies on deprecated or archived assets unless intentionally documented.",
286
+ ],
287
+ llmHint: `Inspect "${target.sourcePath}" for superseded_by or canonical context metadata. If a canonical replacement exists, update ${dependency.kind} reference "${dependency.to}" declared by "${dependency.from}". If not, decide whether the reference should remain and document why.`,
288
+ },
289
+ ];
290
+ });
291
+ }
292
+ function orphanedContextAssetFindings(entries, dependencies, resolver) {
293
+ const referencedPaths = new Set();
294
+ for (const dependency of dependencies) {
295
+ const target = resolver.resolve(dependency.to);
296
+ const source = resolver.resolve(dependency.from);
297
+ if (!target)
298
+ continue;
299
+ if (source?.sourcePath === target.sourcePath)
300
+ continue;
301
+ referencedPaths.add(target.sourcePath);
302
+ }
303
+ return entries.flatMap((entry) => {
304
+ if (!isFirstClassSharedContext(entry))
305
+ return [];
306
+ if (entry.metadata.status === "deprecated" ||
307
+ entry.metadata.status === "archived") {
308
+ return [];
309
+ }
310
+ if (referencedPaths.has(entry.sourcePath))
311
+ return [];
312
+ return [
313
+ {
314
+ id: "MAINT-ORPHANED-CONTEXT-ASSET",
315
+ title: "Shared context asset is not referenced by other assets",
316
+ category: "maintenance",
317
+ severity: "low",
318
+ confidence: "medium",
319
+ evidence: metadataFindingEvidence(entry.sourcePath, "Shared context asset has no incoming declared references."),
320
+ whyItMatters: "Shared context assets are most valuable when discoverable and connected to skills, other contexts, or repository guidance. Orphaned context assets may be unused, newly created but not wired in, or missing declared references.",
321
+ remediation: "If the context is intended to be used, reference it from the relevant skill or context metadata. If it is obsolete, deprecate or archive it. If it is intentionally standalone, document its intended discovery path.",
322
+ constraints: [
323
+ "Do not delete context assets automatically.",
324
+ "Do not require every context asset to be referenced immediately.",
325
+ "Do not make Renma decide runtime context selection.",
326
+ "Use this as a repository maintenance advisory.",
327
+ ],
328
+ verificationSteps: [
329
+ "Run renma scan.",
330
+ "Run renma catalog.",
331
+ "Confirm context is referenced, intentionally standalone, deprecated, or archived.",
332
+ ],
333
+ llmHint: `Search the repository for related skills, contexts, filenames, headings, or domain terms for "${entry.sourcePath}". If this context should be used, add a declared reference from the appropriate skill or context. If obsolete, propose a deprecation or archive patch.`,
334
+ },
335
+ ];
336
+ });
337
+ }
338
+ function createCatalogReferenceResolver(entries) {
339
+ const byId = new Map();
340
+ const byPath = new Map();
341
+ for (const entry of entries) {
342
+ if (!byId.has(entry.id))
343
+ byId.set(entry.id, entry);
344
+ const normalizedPath = normalizeReference(entry.sourcePath);
345
+ if (!byPath.has(normalizedPath))
346
+ byPath.set(normalizedPath, entry);
347
+ }
348
+ return {
349
+ resolve(reference) {
350
+ return byId.get(reference) ?? byPath.get(normalizeReference(reference));
351
+ },
352
+ };
353
+ }
354
+ function normalizeReference(reference) {
355
+ return reference.replace(/\\/g, "/").replace(/^\.\//, "");
356
+ }
357
+ function isFirstClassSharedContext(entry) {
358
+ return (entry.kind === "context" &&
359
+ (entry.sourcePath.startsWith("contexts/") ||
360
+ entry.sourcePath.startsWith("context/")));
361
+ }
362
+ function metadataFindingEvidence(path, snippet) {
363
+ return {
364
+ path,
365
+ startLine: 1,
366
+ endLine: 1,
367
+ snippet,
368
+ };
369
+ }
370
+ /** Return whether a severity is at least as severe as a configured threshold. */
371
+ export function severityMeets(value, threshold) {
372
+ const order = {
373
+ low: 0,
374
+ medium: 1,
375
+ high: 2,
376
+ critical: 3,
377
+ };
378
+ return order[value] >= order[threshold];
379
+ }
380
+ function secretFindings(document) {
381
+ return matchingLineFindings(document, (line) => {
382
+ if (PRIVATE_KEY_PATTERN.test(line)) {
383
+ return finding("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.");
384
+ }
385
+ if (SECRET_PATTERN.test(line) && !isPlaceholder(line)) {
386
+ return finding("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.");
387
+ }
388
+ return undefined;
389
+ });
390
+ }
391
+ function commandFindings(document) {
392
+ return matchingLineFindings(document, (line) => {
393
+ if (isSuppressed(line))
394
+ return undefined;
395
+ if (DESTRUCTIVE_PATTERN.test(line) &&
396
+ !hasNearbyConfirmation(document.lines, line)) {
397
+ return finding("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.");
398
+ }
399
+ if (REMOTE_PATTERN.test(line)) {
400
+ return finding("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.");
401
+ }
402
+ if (ENV_COPY_PATTERN.test(line)) {
403
+ return finding("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.");
404
+ }
405
+ return undefined;
406
+ });
407
+ }
408
+ function shapeFindings(document) {
409
+ if (document.artifact.kind !== "skill" && document.artifact.kind !== "agent")
410
+ return [];
411
+ const text = document.artifact.content.toLowerCase();
412
+ const findings = [];
413
+ const description = document.metadata.description ?? "";
414
+ const tokenCount = approximateTokenCount(document.artifact.content);
415
+ if (!description) {
416
+ findings.push(documentFinding(document, "QUAL-MISSING-DESCRIPTION", "Skill is missing an explicit description", "quality", "medium", "Add frontmatter description so agents can route to the skill intentionally."));
417
+ }
418
+ else if (document.artifact.kind === "skill" &&
419
+ description.length < DESCRIPTION_MIN_CHARS) {
420
+ findings.push(documentFinding(document, "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.`));
421
+ }
422
+ const reusableContextFinding = reusableContextCandidateFinding(document, tokenCount);
423
+ if (reusableContextFinding)
424
+ findings.push(reusableContextFinding);
425
+ if (document.artifact.kind === "skill" && tokenCount > SKILL_TOKEN_LIMIT) {
426
+ findings.push(documentFinding(document, "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.`, {
427
+ 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.",
428
+ constraints: [
429
+ "Do not introduce runtime context resolution.",
430
+ "Do not create prompt packages.",
431
+ "Do not make Renma responsible for selecting context.",
432
+ "Preserve the skill as an LLM-facing entrypoint / usage guide.",
433
+ "Give extracted context assets stable metadata such as id, owner, and status.",
434
+ ],
435
+ verificationSteps: [
436
+ "Run renma scan.",
437
+ "Run any project-specific validation checks that apply to this repository.",
438
+ "Confirm the skill is shorter and extracted knowledge is represented as shared context assets.",
439
+ ],
440
+ llmHint: "If this skill mixes reusable knowledge with usage guidance, split reusable knowledge into first-class context assets under contexts/ and update the skill metadata or text to reference them.",
441
+ }));
442
+ }
443
+ if (document.artifact.kind === "skill" &&
444
+ USER_LOCAL_PATH_PATTERN.test(text)) {
445
+ findings.push(documentFinding(document, "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/...`."));
446
+ }
447
+ if (!/do not use for|non-goals|out of scope/.test(text)) {
448
+ findings.push(documentFinding(document, "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."));
449
+ }
450
+ if (!/use this skill|when to use|trigger|routing|context route|mixin/.test(text)) {
451
+ findings.push(documentFinding(document, "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."));
452
+ }
453
+ if (!/example|input|output/.test(text)) {
454
+ findings.push(documentFinding(document, "QUAL-MISSING-EXAMPLES", "Skill lacks examples", "quality", "low", "Add examples that show representative inputs, outputs, or behavior."));
455
+ }
456
+ if (!/preflight|before you begin|first check|prerequisite|context/.test(text)) {
457
+ findings.push(documentFinding(document, "QUAL-MISSING-PREFLIGHT", "Skill lacks a preflight step", "quality", "medium", "Add a preflight section that captures environment, permissions, target files, and assumptions before acting."));
458
+ }
459
+ if (document.artifact.kind === "skill" &&
460
+ !REQUIRED_INPUTS_PATTERN.test(text)) {
461
+ findings.push(documentFinding(document, "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.", {
462
+ 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.",
463
+ constraints: [
464
+ "Do not infer runtime context.",
465
+ "Do not assemble prompt packages.",
466
+ "Do not require optional context selection.",
467
+ "Do not make Renma decide whether the workflow can run for the current task.",
468
+ "Keep the skill as a static workflow entrypoint.",
469
+ ],
470
+ verificationSteps: [
471
+ "Run renma scan.",
472
+ "Run renma readiness.",
473
+ "Confirm each skill entrypoint either documents required inputs or explicitly states that no special inputs are required.",
474
+ ],
475
+ llmHint: "Add a concise Required inputs or Prerequisites section to this SKILL.md. State user-provided inputs, target files, repository state, permissions, credentials, and environment assumptions needed before the workflow starts. Do not add runtime context selection or prompt assembly behavior.",
476
+ }));
477
+ }
478
+ if (document.artifact.kind === "skill" &&
479
+ !COMPLETION_CRITERIA_PATTERN.test(text)) {
480
+ findings.push(documentFinding(document, "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.", {
481
+ 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.",
482
+ constraints: [
483
+ "Do not infer runtime context.",
484
+ "Do not assemble prompt packages.",
485
+ "Do not require optional context selection.",
486
+ "Do not make Renma decide task-specific success at runtime.",
487
+ "Keep the skill as a static workflow entrypoint.",
488
+ ],
489
+ verificationSteps: [
490
+ "Run renma scan.",
491
+ "Run renma readiness.",
492
+ "Confirm each skill workflow entrypoint documents completion criteria.",
493
+ ],
494
+ llmHint: "Add a concise Completion criteria, Success requirements, Deliverables, or Final response section to this SKILL.md. State the observable outputs, checks, or final-response conditions that mean the workflow is complete. Do not add runtime context selection or prompt assembly behavior.",
495
+ }));
496
+ }
497
+ if (!/verify|validation|test|confirm result|expected output/.test(text)) {
498
+ findings.push(documentFinding(document, "QUAL-MISSING-VERIFICATION", "Skill lacks verification guidance", "quality", "medium", "State how to verify success with a command, check, or observable result."));
499
+ }
500
+ if (document.headings.length < 2 &&
501
+ document.artifact.content.split(/\s+/).length > 120) {
502
+ findings.push(documentFinding(document, "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."));
503
+ }
504
+ return findings;
505
+ }
506
+ function reusableContextCandidateFinding(document, tokenCount) {
507
+ if (document.artifact.kind !== "skill")
508
+ return undefined;
509
+ if (document.lines.length < REUSABLE_CONTEXT_MIN_LINES &&
510
+ tokenCount < REUSABLE_CONTEXT_MIN_TOKENS)
511
+ return undefined;
512
+ const headingMatches = document.headings.flatMap((heading) => REUSABLE_CONTEXT_HEADING_PATTERNS.filter(([pattern]) => pattern.test(heading.text)).map(([, label]) => ({
513
+ label,
514
+ line: heading.line,
515
+ text: heading.text,
516
+ })));
517
+ const phraseMatches = REUSABLE_CONTEXT_PHRASE_PATTERNS.flatMap(([pattern, label]) => {
518
+ const lineIndex = document.lines.findIndex((line) => pattern.test(line));
519
+ if (lineIndex < 0)
520
+ return [];
521
+ return [
522
+ {
523
+ label,
524
+ line: lineIndex + 1,
525
+ text: document.lines[lineIndex]?.trim() ?? label,
526
+ },
527
+ ];
528
+ });
529
+ const headingLabels = [
530
+ ...new Set(headingMatches.map((match) => match.label)),
531
+ ];
532
+ const phraseLabels = [...new Set(phraseMatches.map((match) => match.label))];
533
+ const signalCount = new Set([...headingLabels, ...phraseLabels]).size;
534
+ if (signalCount < REUSABLE_CONTEXT_MIN_SIGNALS)
535
+ return undefined;
536
+ const evidenceLine = headingMatches[0]?.line ??
537
+ phraseMatches[0]?.line ??
538
+ Math.max(1, document.lines.findIndex((line) => line.trim().length > 0) + 1);
539
+ const evidenceParts = [
540
+ headingLabels.length > 0
541
+ ? `Detected reusable-knowledge headings: ${headingLabels
542
+ .slice(0, 5)
543
+ .join(" - ")}`
544
+ : undefined,
545
+ phraseLabels.length > 0
546
+ ? `Detected reusable-knowledge phrases: ${phraseLabels
547
+ .slice(0, 5)
548
+ .join(" - ")}`
549
+ : undefined,
550
+ ].filter((part) => Boolean(part));
551
+ return {
552
+ id: "MAINT-SKILL-REUSABLE-CONTEXT-CANDIDATE",
553
+ title: "Skill may contain reusable context worth extracting",
554
+ category: "maintenance",
555
+ severity: "low",
556
+ confidence: "medium",
557
+ evidence: evidence(document, evidenceLine, evidenceParts.join("; ")),
558
+ whyItMatters: "Reusable setup notes, troubleshooting, platform guidance, testing heuristics, or domain rules are easier to own, review, and reuse when they live in shared context assets instead of only inside one skill.",
559
+ remediation: "Review the matched headings and phrases. If they describe reusable knowledge, extract that knowledge into first-class shared context assets under contexts/ and keep SKILL.md as a concise LLM-facing usage guide.",
560
+ constraints: [
561
+ "Do not make Renma select runtime context.",
562
+ "Do not assemble prompt packages.",
563
+ "Do not automatically rewrite or split skills.",
564
+ "Preserve SKILL.md as the routing contract / usage guide.",
565
+ "Give extracted context assets stable metadata such as id, owner, and status.",
566
+ ],
567
+ verificationSteps: [
568
+ "Run renma scan.",
569
+ "Confirm the advisory is resolved or intentionally accepted after reusable knowledge is represented as shared context assets.",
570
+ ],
571
+ llmHint: "Look for reusable setup, troubleshooting, platform, testing, or domain guidance in this skill. If reusable, move it into owned contexts/ assets and update the skill to reference those assets without adding runtime context selection.",
572
+ };
573
+ }
574
+ function supportSharedContextCandidateFindings(document) {
575
+ if (document.artifact.kind !== "reference")
576
+ return [];
577
+ if (!/^skills\/[^/]+\/references\/.+\.md$/u.test(document.artifact.path)) {
578
+ return [];
579
+ }
580
+ const tokenCount = approximateTokenCount(document.artifact.content);
581
+ if (document.lines.length < SUPPORT_SHARED_CONTEXT_MIN_LINES &&
582
+ tokenCount < SUPPORT_SHARED_CONTEXT_MIN_TOKENS) {
583
+ return [];
584
+ }
585
+ const contentLineIndexes = markdownBodyLineIndexes(document);
586
+ const headingMatches = SUPPORT_SHARED_CONTEXT_HEADING_PATTERNS.flatMap(([pattern, label]) => {
587
+ const lineIndex = contentLineIndexes.find((index) => {
588
+ const line = document.lines[index] ?? "";
589
+ const match = line.match(/^#{1,6}\s+(.+)$/u);
590
+ return match ? pattern.test(match[1] ?? "") : false;
591
+ });
592
+ if (lineIndex === undefined)
593
+ return [];
594
+ return [
595
+ {
596
+ label,
597
+ line: lineIndex + 1,
598
+ text: document.lines[lineIndex]?.trim() ?? label,
599
+ type: "heading",
600
+ },
601
+ ];
602
+ });
603
+ const phraseMatches = SUPPORT_SHARED_CONTEXT_PHRASE_PATTERNS.flatMap(([pattern, label]) => {
604
+ const lineIndex = contentLineIndexes.find((index) => pattern.test(document.lines[index] ?? ""));
605
+ if (lineIndex === undefined)
606
+ return [];
607
+ return [
608
+ {
609
+ label,
610
+ line: lineIndex + 1,
611
+ text: document.lines[lineIndex]?.trim() ?? label,
612
+ type: "phrase",
613
+ },
614
+ ];
615
+ });
616
+ const sourceSignals = [...headingMatches, ...phraseMatches];
617
+ if (headingMatches.length < SUPPORT_SHARED_CONTEXT_MIN_HEADINGS ||
618
+ phraseMatches.length < SUPPORT_SHARED_CONTEXT_MIN_PHRASES) {
619
+ return [];
620
+ }
621
+ const evidenceMatches = sourceSignals
622
+ .slice(0, 12)
623
+ .sort((a, b) => a.line - b.line);
624
+ const evidenceLine = evidenceMatches[0]?.line ?? 1;
625
+ const evidenceSnippet = [
626
+ "Detected source-of-truth headings:",
627
+ ...headingMatches.slice(0, 8).map((match) => `- ${match.label}`),
628
+ "Detected reusable guidance phrases:",
629
+ ...phraseMatches.slice(0, 8).map((match) => `- ${match.label}`),
630
+ "Evidence lines:",
631
+ ...[...sourceSignals]
632
+ .sort((a, b) => a.line - b.line)
633
+ .slice(0, 8)
634
+ .map((match) => `- ${match.type}: ${match.label} (line ${match.line}) ${match.text}`),
635
+ ].join("\n");
636
+ return [
637
+ {
638
+ id: "MAINT-SUPPORT-ASSET-SHARED-CONTEXT-CANDIDATE",
639
+ title: "Skill-local support file may be a shared context candidate",
640
+ category: "maintenance",
641
+ severity: "low",
642
+ confidence: "medium",
643
+ evidence: evidence(document, evidenceLine, evidenceSnippet),
644
+ whyItMatters: "Skill-local references are useful for local support, but reusable source-of-truth knowledge is easier to own, review, and reuse when represented as a first-class shared context asset under contexts/. Large support files with setup, decision logic, troubleshooting, validation, constraints, or policy-like guidance may be useful beyond one skill.",
645
+ remediation: "Review this support file and decide whether reusable knowledge should be promoted to a shared context asset under contexts/. Keep only skill-specific reading order, local notes, or one-off examples under skills/*/references/. Update declared context references after any promotion.",
646
+ constraints: [
647
+ "Do not introduce runtime context resolution.",
648
+ "Do not create prompt packages.",
649
+ "Do not make Renma call an LLM.",
650
+ "Do not move files automatically as part of scan.",
651
+ "Do not delete or summarize procedural details.",
652
+ "Preserve skill-local references when they are truly local to one skill.",
653
+ "Give promoted context assets stable metadata such as id, owner, and status.",
654
+ ],
655
+ verificationSteps: [
656
+ "Run renma scan.",
657
+ "Run renma catalog.",
658
+ "Run any project-specific validation checks that apply to this repository.",
659
+ "Confirm reusable source-of-truth knowledge lives in contexts/ and skill-local references only contain local support guidance.",
660
+ ],
661
+ llmHint: "Search the repository for similar headings, filenames, repeated procedures, commands, constraints, or overlapping guidance. If this support file appears reusable, propose a first-class context asset under contexts/, move the reusable details without losing information, keep truly local notes in the skill directory, and update declared context references.",
662
+ },
663
+ ];
664
+ }
665
+ function contextPathNonSemanticFindings(document) {
666
+ if (document.artifact.kind !== "context")
667
+ return [];
668
+ const segments = document.artifact.path.split("/");
669
+ const root = segments[0];
670
+ if (root !== "context" && root !== "contexts")
671
+ return [];
672
+ const suspiciousSegment = segments
673
+ .slice(1, -1)
674
+ .find((segment) => NON_SEMANTIC_CONTEXT_PATH_SEGMENTS.has(segment.toLowerCase()));
675
+ if (!suspiciousSegment)
676
+ return [];
677
+ return [
678
+ {
679
+ id: "MAINT-CONTEXT-PATH-NON-SEMANTIC",
680
+ title: "Context asset path appears process-oriented rather than semantic",
681
+ category: "maintenance",
682
+ severity: "low",
683
+ confidence: "high",
684
+ evidence: evidence(document, 1, `Path segment "${suspiciousSegment}" appears process-oriented. Consider a semantic context path.`),
685
+ whyItMatters: "Shared context assets should be discoverable by their meaning, ownership, domain, tool, team, or policy scope. Process-state folders such as promoted, generated, or drafts describe how a file was created rather than what knowledge it owns, which makes the repository harder for humans and agents to navigate over time.",
686
+ remediation: "Move this context asset to a semantic path that reflects its source-of-truth scope. Prefer paths such as contexts/tools/<tool>/..., contexts/domain/<domain>/..., contexts/testing/..., contexts/teams/<team>/..., or contexts/policies/.... Update any declared context references after moving the file.",
687
+ constraints: [
688
+ "Do not introduce runtime context resolution.",
689
+ "Do not create prompt packages.",
690
+ "Do not make Renma call an LLM.",
691
+ "Do not move files automatically as part of scan.",
692
+ "Preserve the context content and metadata.",
693
+ "Update references only through a reviewable human or calling-agent patch.",
694
+ "Temporary staging folders are acceptable outside final contexts/ paths, but final shared context assets should use semantic paths.",
695
+ ],
696
+ verificationSteps: [
697
+ "Run renma scan.",
698
+ "Run renma catalog.",
699
+ "Run project-specific validation checks that apply to this repository.",
700
+ "Confirm the context asset now lives under a semantic path and declared references still point to it correctly.",
701
+ ],
702
+ llmHint: "Infer semantic scope from context title, headings, metadata, and references. Propose a path based on meaning, ownership, or reuse domain, such as contexts/tools/<tool>/..., contexts/domain/<domain>/..., contexts/testing/..., contexts/teams/<team>/..., or contexts/policies/.... Avoid final folders named after migration state such as promoted or generated.",
703
+ },
704
+ ];
705
+ }
706
+ function skillContextReferenceNotDeclaredFindings(document) {
707
+ if (document.artifact.kind !== "skill")
708
+ return [];
709
+ const declaredContexts = new Set(listMetadataValue(document.metadata.requires_context));
710
+ const bodyLineIndexes = markdownBodyLineIndexes(document);
711
+ const matches = new Map();
712
+ for (const index of bodyLineIndexes) {
713
+ const line = document.lines[index] ?? "";
714
+ for (const match of line.matchAll(/\bcontexts?\/[^\s`)'"]+\.md\b/gu)) {
715
+ const referencedPath = match[0];
716
+ if (!matches.has(referencedPath)) {
717
+ matches.set(referencedPath, {
718
+ line: index + 1,
719
+ text: line.trim(),
720
+ });
721
+ }
722
+ }
723
+ }
724
+ return [...matches.entries()]
725
+ .filter(([referencedPath]) => !declaredContexts.has(referencedPath))
726
+ .map(([referencedPath, match]) => ({
727
+ id: "MAINT-SKILL-CONTEXT-REFERENCE-NOT-DECLARED",
728
+ title: "Skill references a shared context without declaring it",
729
+ category: "maintenance",
730
+ severity: "low",
731
+ confidence: "high",
732
+ evidence: evidence(document, match.line, match.text),
733
+ whyItMatters: "Declared context references make skill/context relationships visible to catalog, graph, and validation reports. If a skill only mentions a context in prose, humans may see the dependency but repository tooling cannot validate it.",
734
+ remediation: "Add the referenced shared context asset to the skill metadata using requires_context, or remove the prose reference if it is no longer needed.",
735
+ constraints: [
736
+ "Do not select runtime context.",
737
+ "Do not assemble prompt packages.",
738
+ "Do not make Renma decide which context a task should use.",
739
+ "Only declare repository relationships that the skill already references or intentionally depends on.",
740
+ ],
741
+ verificationSteps: [
742
+ "Run renma scan.",
743
+ "Run renma catalog.",
744
+ "Confirm the skill/context relationship appears in metadata and catalog output.",
745
+ ],
746
+ llmHint: `Find context paths mentioned in the skill body and add them to requires_context using the metadata syntax currently supported by Renma. Missing declaration: ${referencedPath}`,
747
+ }));
748
+ }
749
+ function skillReferencesSupersededAssetFindings(documents) {
750
+ const skillsByPath = new Map(documents
751
+ .filter((document) => document.artifact.kind === "skill")
752
+ .map((document) => [document.artifact.path, document]));
753
+ return documents.flatMap((document) => {
754
+ if (!isSkillLocalReference(document))
755
+ return [];
756
+ const canonicalTargets = sharedContextTargets(document);
757
+ const supersededStatus = document.metadata.status === "deprecated" ||
758
+ document.metadata.status === "archived";
759
+ if (!supersededStatus && canonicalTargets.length === 0)
760
+ return [];
761
+ if (canonicalTargets.length === 0)
762
+ return [];
763
+ const skillPath = parentSkillPath(document.artifact.path);
764
+ if (!skillPath)
765
+ return [];
766
+ const skill = skillsByPath.get(skillPath);
767
+ if (!skill)
768
+ return [];
769
+ const referencedFrom = skillReferenceLine(skill, document.artifact.path);
770
+ if (!referencedFrom)
771
+ return [];
772
+ const canonicalTargetList = canonicalTargets
773
+ .map((target) => `- ${target}`)
774
+ .join("\n");
775
+ const snippet = [
776
+ `Deprecated local support file: ${document.artifact.path}`,
777
+ "Superseded by:",
778
+ canonicalTargetList,
779
+ `Referenced from: ${skill.artifact.path}`,
780
+ referencedFrom.text,
781
+ ].join("\n");
782
+ return [
783
+ {
784
+ id: "MAINT-SKILL-REFERENCES-SUPERSEDED-ASSET",
785
+ title: "Skill references a superseded local support asset",
786
+ category: "maintenance",
787
+ severity: "low",
788
+ confidence: "medium",
789
+ evidence: evidence(skill, referencedFrom.line, snippet),
790
+ whyItMatters: "Deprecated or superseded local support files can be useful as compatibility shims, but keeping them in a primary reading path may hide the canonical shared context asset from humans and agents. Shared context assets should be the visible source of truth when reusable knowledge has been promoted to contexts/.",
791
+ remediation: "Update the skill to reference the canonical shared context asset directly, or keep the deprecated local support file only as a clearly documented compatibility shim. If the local file still contains unique skill-specific guidance, reduce it to that local guidance and point to the shared context for reusable knowledge.",
792
+ constraints: [
793
+ "Do not introduce runtime context resolution.",
794
+ "Do not create prompt packages.",
795
+ "Do not make Renma call an LLM.",
796
+ "Do not automatically move or delete files during scan.",
797
+ "Preserve compatibility shims if they are still needed.",
798
+ "Preserve unique skill-local guidance if it is not reusable shared context.",
799
+ "Update declared context references when pointing the skill directly at shared contexts.",
800
+ ],
801
+ verificationSteps: [
802
+ "Run renma scan.",
803
+ "Run renma catalog.",
804
+ "Run project-specific validation checks that apply to this repository.",
805
+ "Confirm the skill points to canonical shared context assets directly, or any deprecated local support file is clearly only a compatibility shim.",
806
+ ],
807
+ llmHint: "Inspect the deprecated local support file and its superseded_by or canonical_context metadata. If the shared context asset is now canonical, update skill guidance and metadata to reference the shared context directly. Keep the local reference only if it contains truly local notes or is intentionally preserved as a compatibility shim.",
808
+ },
809
+ ];
810
+ });
811
+ }
812
+ function isSkillLocalReference(document) {
813
+ return (document.artifact.kind === "reference" &&
814
+ /^skills\/[^/]+\/references\/.+\.md$/u.test(document.artifact.path));
815
+ }
816
+ function sharedContextTargets(document) {
817
+ return [
818
+ ...listMetadataValue(document.metadata.superseded_by),
819
+ ...listMetadataValue(document.metadata.canonical_context),
820
+ ].filter((target, index, targets) => /^contexts?\//u.test(target) && targets.indexOf(target) === index);
821
+ }
822
+ function parentSkillPath(referencePath) {
823
+ const segments = referencePath.split("/");
824
+ if (segments.length < 4 || segments[0] !== "skills")
825
+ return undefined;
826
+ return `skills/${segments[1]}/SKILL.md`;
827
+ }
828
+ function skillReferenceLine(skill, referencePath) {
829
+ const skillDir = path.posix.dirname(skill.artifact.path);
830
+ const relativePath = path.posix.relative(skillDir, referencePath);
831
+ const referencedTokens = [referencePath, relativePath];
832
+ for (const index of markdownBodyLineIndexes(skill)) {
833
+ const line = skill.lines[index] ?? "";
834
+ if (referencedTokens.some((token) => line.includes(token))) {
835
+ return { line: index + 1, text: line.trim() };
836
+ }
837
+ }
838
+ return undefined;
839
+ }
840
+ function assetReferencesSupersededAssetFindings(documents) {
841
+ const supersededAssets = documents
842
+ .map((document) => ({
843
+ document,
844
+ canonicalTargets: sharedContextTargets(document),
845
+ }))
846
+ .filter(({ document, canonicalTargets }) => document.metadata.status === "deprecated" ||
847
+ document.metadata.status === "archived" ||
848
+ canonicalTargets.length > 0)
849
+ .filter(({ canonicalTargets }) => canonicalTargets.length > 0);
850
+ return documents.flatMap((referencingDocument) => {
851
+ if (referencingDocument.artifact.kind === "skill")
852
+ return [];
853
+ return supersededAssets.flatMap(({ document, canonicalTargets }) => {
854
+ if (document.artifact.path === referencingDocument.artifact.path) {
855
+ return [];
856
+ }
857
+ const reference = assetReferenceLine(referencingDocument, document.artifact.path);
858
+ if (!reference)
859
+ return [];
860
+ const canonicalTargetList = canonicalTargets
861
+ .map((target) => `- ${target}`)
862
+ .join("\n");
863
+ const snippet = [
864
+ `Referencing asset: ${referencingDocument.artifact.path}`,
865
+ `Referenced superseded asset: ${document.artifact.path}`,
866
+ "Superseded by:",
867
+ canonicalTargetList,
868
+ reference.text,
869
+ ].join("\n");
870
+ return [
871
+ {
872
+ id: "MAINT-ASSET-REFERENCES-SUPERSEDED-ASSET",
873
+ title: "Asset references a superseded support file",
874
+ category: "maintenance",
875
+ severity: "low",
876
+ confidence: "medium",
877
+ evidence: evidence(referencingDocument, reference.line, snippet),
878
+ whyItMatters: "Deprecated or superseded support files may remain as compatibility shims, but assets that keep referencing them can hide the canonical shared context asset from humans and agents. Once reusable knowledge has been promoted to contexts/, repository assets should usually reference the canonical context directly.",
879
+ remediation: "Update this asset to reference the canonical shared context asset directly, or keep the superseded reference only if it is intentionally needed as a compatibility shim. If the deprecated file still contains unique local guidance, preserve that local guidance and point reusable knowledge to the canonical context.",
880
+ constraints: [
881
+ "Do not introduce runtime context resolution.",
882
+ "Do not create prompt packages.",
883
+ "Do not make Renma call an LLM.",
884
+ "Do not automatically move or rewrite files during scan.",
885
+ "Preserve compatibility shims if they are intentionally needed.",
886
+ "Preserve unique local guidance not reusable shared context.",
887
+ "Update references through a reviewable human or calling-agent patch.",
888
+ ],
889
+ verificationSteps: [
890
+ "Run renma scan.",
891
+ "Run renma catalog.",
892
+ "Run project-specific validation checks that apply to this repository.",
893
+ "Confirm referencing asset now points to the canonical shared context asset, or documents why the superseded shim is still needed.",
894
+ ],
895
+ llmHint: "Inspect the referenced deprecated asset and its superseded_by or canonical context metadata. If the canonical shared context is the intended source of truth, update this asset to reference that context directly. Keep the superseded file only when it serves a deliberate compatibility or migration role.",
896
+ },
897
+ ];
898
+ });
899
+ });
900
+ }
901
+ function assetReferenceLine(referencingDocument, targetPath) {
902
+ const referencingDir = path.posix.dirname(referencingDocument.artifact.path);
903
+ const relativePath = path.posix.relative(referencingDir, targetPath);
904
+ const referencedTokens = uniqueStrings([
905
+ targetPath,
906
+ relativePath,
907
+ skillRelativePath(referencingDocument.artifact.path, targetPath),
908
+ ]).filter(Boolean);
909
+ for (const index of markdownBodyLineIndexes(referencingDocument)) {
910
+ const line = referencingDocument.lines[index] ?? "";
911
+ if (referencedTokens.some((token) => line.includes(token))) {
912
+ return { line: index + 1, text: line.trim() };
913
+ }
914
+ }
915
+ return undefined;
916
+ }
917
+ function skillRelativePath(referencingPath, targetPath) {
918
+ const referencingSegments = referencingPath.split("/");
919
+ const targetSegments = targetPath.split("/");
920
+ if (referencingSegments[0] !== "skills" ||
921
+ targetSegments[0] !== "skills" ||
922
+ referencingSegments[1] !== targetSegments[1]) {
923
+ return undefined;
924
+ }
925
+ return targetSegments.slice(2).join("/");
926
+ }
927
+ function uniqueStrings(values) {
928
+ return values.filter((value, index) => Boolean(value) && values.indexOf(value) === index);
929
+ }
930
+ function contextBudgetFindings(document) {
931
+ if (document.artifact.kind !== "context" &&
932
+ document.artifact.kind !== "profile" &&
933
+ document.artifact.kind !== "reference" &&
934
+ document.artifact.kind !== "example") {
935
+ return [];
936
+ }
937
+ const limit = CONTEXT_TOKEN_LIMITS[document.artifact.kind];
938
+ const tokenCount = approximateTokenCount(document.artifact.content);
939
+ if (tokenCount <= limit)
940
+ return [];
941
+ return [
942
+ documentFinding(document, "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.`, {
943
+ 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.",
944
+ constraints: [
945
+ "Do not introduce runtime context resolution.",
946
+ "Do not create prompt packages.",
947
+ "Preserve concrete procedural steps losslessly.",
948
+ "Keep static references from the parent file or SKILL.md to every split part.",
949
+ ],
950
+ verificationSteps: [
951
+ "Run renma scan.",
952
+ "Run the repository-specific validation or test command, if one exists.",
953
+ "Confirm the finding is resolved or reduced and every split part remains reachable.",
954
+ ],
955
+ llmHint: "Split oversized support content into meaning-based ordered part files, keep the original procedure text intact, and update static references so Renma can validate reachability.",
956
+ }),
957
+ ];
958
+ }
959
+ function profileFindings(document) {
960
+ if (document.artifact.kind !== "profile")
961
+ return [];
962
+ const text = document.artifact.content.toLowerCase();
963
+ if (/base[_ -]?skill|extends/.test(text))
964
+ return [];
965
+ return [
966
+ documentFinding(document, "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."),
967
+ ];
968
+ }
969
+ function skillLocalSupportReachabilityFindings(documents) {
970
+ const skills = documents.filter((document) => document.artifact.kind === "skill");
971
+ return skills.flatMap((skill) => {
972
+ const skillDir = path.posix.dirname(skill.artifact.path);
973
+ const localSupportDocs = documents.filter((document) => ["profile", "reference", "example"].includes(document.artifact.kind) &&
974
+ document.artifact.path.startsWith(`${skillDir}/`));
975
+ if (localSupportDocs.length === 0)
976
+ return [];
977
+ const findings = [];
978
+ const text = skill.artifact.content.toLowerCase();
979
+ const hasLocalSupportGuidance = /support file|local support|context route|context map|mixin|profiles?\/|references?\/|examples?\/|load .*?(?:profile|reference|example)|reference .*?(?:profile|reference|example)/.test(text);
980
+ if (!hasLocalSupportGuidance) {
981
+ findings.push(documentFinding(skill, "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.", {
982
+ 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.",
983
+ constraints: [
984
+ "Do not introduce runtime context resolution.",
985
+ "Do not make Renma responsible for selecting context.",
986
+ "Use static repository references from SKILL.md to local support files or their index.",
987
+ "Preserve original concrete steps and support content.",
988
+ ],
989
+ verificationSteps: [
990
+ "Run renma scan.",
991
+ "Run any project-specific validation checks that apply to this repository.",
992
+ "Confirm each local profile, reference, or example is reachable from SKILL.md or from a referenced parent support file.",
993
+ ],
994
+ llmHint: "Add concise reachability guidance in SKILL.md that references local profiles, references, examples, or ordered support indexes without adding runtime routing behavior.",
995
+ }));
996
+ }
997
+ const reachableLocalSupportPaths = reachableLocalSupportDocuments(skill, localSupportDocs);
998
+ for (const document of localSupportDocs) {
999
+ if (!reachableLocalSupportPaths.has(document.artifact.path)) {
1000
+ findings.push(documentFinding(document, localSupportUnreachableRuleId(document.artifact.kind), "Local support file is not reachable from the skill", "structure", "low", "Reference this local support file from SKILL.md or from a referenced parent support file with clear reachability guidance. If this file is a split part, ensure the parent skill references the index or all ordered parts so preserved details remain reachable. Do not delete, summarize, or merge away procedural steps just to satisfy the check.", {
1001
+ whyItMatters: "Unreachable local support files can drift outside review and be missed by humans or LLM coding agents. Reachability should be static repository evidence, not runtime context assembly.",
1002
+ constraints: [
1003
+ "Do not introduce runtime context resolution.",
1004
+ "Do not delete or summarize support content just to satisfy the check.",
1005
+ "Preserve ordered split parts and concrete procedural details.",
1006
+ "Use SKILL.md or a referenced parent support file for static reachability.",
1007
+ ],
1008
+ verificationSteps: [
1009
+ "Run renma scan.",
1010
+ "Run any project-specific validation checks that apply to this repository.",
1011
+ "Confirm this support file is no longer reported as unreachable.",
1012
+ ],
1013
+ llmHint: "Update SKILL.md or a referenced support index to mention this file by path, basename, or clear title so the static reachability graph can find it.",
1014
+ }));
1015
+ }
1016
+ }
1017
+ return findings;
1018
+ });
1019
+ }
1020
+ function reachableLocalSupportDocuments(skill, localSupportDocs) {
1021
+ const reachable = new Set();
1022
+ let changed = true;
1023
+ while (changed) {
1024
+ changed = false;
1025
+ for (const document of localSupportDocs) {
1026
+ if (reachable.has(document.artifact.path))
1027
+ continue;
1028
+ const possibleRouters = [
1029
+ skill,
1030
+ ...localSupportDocs.filter((candidate) => reachable.has(candidate.artifact.path)),
1031
+ ];
1032
+ if (possibleRouters.some((router) => referencesDocument(router, document))) {
1033
+ reachable.add(document.artifact.path);
1034
+ changed = true;
1035
+ }
1036
+ }
1037
+ }
1038
+ return reachable;
1039
+ }
1040
+ function referencesDocument(source, target) {
1041
+ const name = path.posix.basename(target.artifact.path, path.posix.extname(target.artifact.path));
1042
+ const basename = path.posix.basename(target.artifact.path);
1043
+ return (source.artifact.content.includes(target.artifact.path) ||
1044
+ source.artifact.content.includes(basename) ||
1045
+ new RegExp(`\\b${escapeRegExp(name)}\\b`, "i").test(source.artifact.content));
1046
+ }
1047
+ function markdownBodyLineIndexes(document) {
1048
+ if (document.lines[0]?.trim() !== "---") {
1049
+ return document.lines.map((_, index) => index);
1050
+ }
1051
+ const frontmatterEnd = document.lines.findIndex((line, index) => index > 0 && line.trim() === "---");
1052
+ const bodyStart = frontmatterEnd >= 0 ? frontmatterEnd + 1 : 0;
1053
+ return document.lines
1054
+ .map((_, index) => index)
1055
+ .filter((index) => index >= bodyStart);
1056
+ }
1057
+ function listMetadataValue(value) {
1058
+ if (!value)
1059
+ return [];
1060
+ return value
1061
+ .split(",")
1062
+ .map((item) => item.trim())
1063
+ .filter(Boolean);
1064
+ }
1065
+ function matchingLineFindings(document, matcher) {
1066
+ return document.lines.flatMap((line, index) => {
1067
+ const partial = matcher(line);
1068
+ if (!partial)
1069
+ return [];
1070
+ return [
1071
+ {
1072
+ ...partial,
1073
+ evidence: evidence(document, index + 1, line),
1074
+ remediation: partial.remediation,
1075
+ },
1076
+ ];
1077
+ });
1078
+ }
1079
+ function strictLayoutPolicyFindings(documents, config, catalog) {
1080
+ const findings = [];
1081
+ const root = repositoryRoot(documents);
1082
+ const paths = new Set(documents.map((document) => document.artifact.path));
1083
+ for (const document of documents) {
1084
+ findings.push(...disallowedSkillAssetFindings(document, config));
1085
+ findings.push(...thinSkillLayoutFindings(document));
1086
+ findings.push(...helperCommandFindings(document, root, paths, config));
1087
+ findings.push(...layoutConsistencyFindings(document));
1088
+ findings.push(...contextRootFindings(document));
1089
+ findings.push(...helperRootFindings(document));
1090
+ }
1091
+ if (catalog) {
1092
+ findings.push(...declaredDependencyLayoutFindings(catalog, paths, root));
1093
+ }
1094
+ return findings;
1095
+ }
1096
+ function disallowedSkillAssetFindings(document, config) {
1097
+ const match = document.artifact.path.match(/^skills\/([^/]+)\/(references|profiles|examples|scripts)\/(.+)$/);
1098
+ if (!match)
1099
+ return [];
1100
+ const [, skillName = "", assetRoot = "", rest = ""] = match;
1101
+ const target = canonicalSkillAssetTarget(config, skillName, assetRoot, rest);
1102
+ return [
1103
+ documentFinding(document, "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.`, {
1104
+ whyItMatters: "Strict layout keeps skills as thin entrypoints, shared context under contexts/, and executable helpers under tools/ so agents can migrate repositories deterministically.",
1105
+ verificationSteps: [
1106
+ `Move ${document.artifact.path} to ${target}.`,
1107
+ "Run renma scan and renma readiness again.",
1108
+ ],
1109
+ llmHint: `Move ${document.artifact.path} to ${target}, then rewrite references from the old path to the new path. Do not leave a compatibility shim unless another Renma finding requires it.`,
1110
+ }),
1111
+ ];
1112
+ }
1113
+ function canonicalSkillAssetTarget(config, skillName, assetRoot, rest) {
1114
+ const workflow = canonicalWorkflowName(config, skillName);
1115
+ if (assetRoot === "scripts")
1116
+ return helperAssetPath(config, workflow, `scripts/${rest}`);
1117
+ return contextAssetPath(config, workflow, `${assetRoot}/${rest}`);
1118
+ }
1119
+ function canonicalWorkflowName(config, skillName) {
1120
+ return config.layout.workflowAliases[skillName] ?? skillName;
1121
+ }
1122
+ function thinSkillLayoutFindings(document) {
1123
+ if (!isCanonicalSkillEntrypoint(document.artifact.path))
1124
+ return [];
1125
+ const findings = [];
1126
+ const wordCount = words(document.artifact.content).length;
1127
+ const procedureSignals = [
1128
+ /^#{2,}\s+(procedure|steps?|install|configure|troubleshoot|diagnose|setup)\b/im,
1129
+ /\b(step\s+\d+|first,|next,|finally)\b/i,
1130
+ ];
1131
+ if (wordCount > 450 ||
1132
+ procedureSignals.some((pattern) => pattern.test(document.artifact.content))) {
1133
+ findings.push(documentFinding(document, "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.", {
1134
+ whyItMatters: "Strict layout expects skills/*/SKILL.md to route agents, not own canonical references, profiles, examples, scripts, or detailed procedures.",
1135
+ verificationSteps: [
1136
+ "Confirm SKILL.md contains routing guidance and required context references only.",
1137
+ "Run renma readiness and check layout.skills_thin.",
1138
+ ],
1139
+ llmHint: "Extract long procedure sections into contexts/**, add requires_context or optional_context references, and keep SKILL.md focused on when to use or not use the skill.",
1140
+ }));
1141
+ }
1142
+ const command = firstExecutableCommand(document);
1143
+ if (command) {
1144
+ findings.push(findingAt(document, "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.", {
1145
+ whyItMatters: "Executable setup commands in SKILL.md make the entrypoint procedural and harder to migrate to the strict three-root layout.",
1146
+ verificationSteps: [
1147
+ "Move the command guidance to a context/reference/procedure.",
1148
+ "If the command invokes a helper script, ensure it points under tools/**.",
1149
+ ],
1150
+ }));
1151
+ }
1152
+ return findings;
1153
+ }
1154
+ function helperCommandFindings(document, root, paths, config) {
1155
+ const findings = [];
1156
+ for (const command of executableCommands(document)) {
1157
+ const scriptPath = helperScriptPath(command.command);
1158
+ if (!scriptPath)
1159
+ continue;
1160
+ if (/^skills\/[^/]+\/scripts\//.test(scriptPath)) {
1161
+ findings.push(findingAt(document, "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.`, {
1162
+ whyItMatters: "Strict layout keeps executable helper assets under tools/**, not under skills/**.",
1163
+ verificationSteps: [
1164
+ `Confirm ${canonicalHelperTarget(config, scriptPath)} exists.`,
1165
+ "Run renma scan and renma readiness again.",
1166
+ ],
1167
+ }));
1168
+ continue;
1169
+ }
1170
+ if (scriptPath.includes("/scripts/") && !scriptPath.startsWith("tools/")) {
1171
+ findings.push(findingAt(document, "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/**.", {
1172
+ whyItMatters: "Helper commands should resolve to non-context helper assets in tools/** so contexts remain LLM-readable guidance.",
1173
+ }));
1174
+ continue;
1175
+ }
1176
+ if (scriptPath.startsWith("tools/") &&
1177
+ !paths.has(scriptPath) &&
1178
+ !(root && existsSync(path.join(root, scriptPath)))) {
1179
+ findings.push(findingAt(document, "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.`, {
1180
+ whyItMatters: "Agents need helper commands in markdown procedures to resolve deterministically before running them.",
1181
+ }));
1182
+ }
1183
+ }
1184
+ return findings;
1185
+ }
1186
+ function layoutConsistencyFindings(document) {
1187
+ if (document.artifact.path !== "README.md" &&
1188
+ document.artifact.path !== "AGENTS.md")
1189
+ return [];
1190
+ const text = document.artifact.content;
1191
+ const findings = [];
1192
+ const stalePatterns = [
1193
+ {
1194
+ pattern: /copy-paste prompt templates/i,
1195
+ message: "Replace copy-paste prompt template wording with execution rules, routing guidance, workflow sections, or actual template assets.",
1196
+ },
1197
+ {
1198
+ pattern: /Each skill includes a self-improvement prompt/i,
1199
+ message: "Describe self-improvement prompts as loaded context/reference/procedure workflow prompts instead of SKILL.md-only content.",
1200
+ },
1201
+ {
1202
+ pattern: /skills\/[^/\s`]+\/(?:references|profiles|examples|scripts)\//,
1203
+ message: "Update docs to point to canonical contexts/** assets or tools/** helper scripts instead of skill-local support directories.",
1204
+ },
1205
+ {
1206
+ pattern: /\]\(contexts\/tools\//,
1207
+ message: "Use backtick repo-root paths for contexts/tools/... references instead of file-relative Markdown links.",
1208
+ },
1209
+ ];
1210
+ for (const stale of stalePatterns) {
1211
+ const match = text.match(stale.pattern);
1212
+ if (!match)
1213
+ continue;
1214
+ const line = lineForOffset(text, match.index ?? 0);
1215
+ findings.push(findingAt(document, "DOCS-LAYOUT-INCONSISTENT", "Repository docs describe a non-canonical layout", "maintenance", "low", line, lineText(document, line), stale.message, {
1216
+ whyItMatters: "README.md and AGENTS.md should teach agents the same strict three-root layout that Renma enforces.",
1217
+ verificationSteps: [
1218
+ "Update README.md and AGENTS.md to mention skills/, contexts/, and tools/ canonical roots.",
1219
+ "Run renma scan again.",
1220
+ ],
1221
+ }));
1222
+ }
1223
+ return findings;
1224
+ }
1225
+ function contextRootFindings(document) {
1226
+ if (document.artifact.path.startsWith("context/")) {
1227
+ return [
1228
+ documentFinding(document, "LAYOUT-CONTEXT-LEGACY-ROOT", "Context asset uses legacy context/ root", "structure", "low", "Move canonical LLM-readable context assets under contexts/**.", {
1229
+ whyItMatters: "The strict repository layout uses contexts/**/*.md as canonical LLM-readable context assets.",
1230
+ }),
1231
+ ];
1232
+ }
1233
+ return [];
1234
+ }
1235
+ function helperRootFindings(document) {
1236
+ if (document.artifact.path.includes("/scripts/") &&
1237
+ !document.artifact.path.startsWith("tools/")) {
1238
+ return [
1239
+ documentFinding(document, "LAYOUT-HELPER-NON_TOOLS", "Helper script is outside tools root", "structure", "medium", "Move non-context helper assets under tools/** and update command references.", {
1240
+ whyItMatters: "The strict repository layout reserves tools/** for executable and non-context helper assets.",
1241
+ }),
1242
+ ];
1243
+ }
1244
+ return [];
1245
+ }
1246
+ function declaredDependencyLayoutFindings(catalog, paths, root) {
1247
+ const findings = [];
1248
+ for (const dependency of catalog.dependencies) {
1249
+ const target = dependency.to;
1250
+ if (!isRepoPathLike(target))
1251
+ continue;
1252
+ if (!paths.has(target) && !(root && existsSync(path.join(root, target)))) {
1253
+ continue;
1254
+ }
1255
+ if (target.startsWith("contexts/") ||
1256
+ target.startsWith("skills/") ||
1257
+ target.startsWith("tools/")) {
1258
+ continue;
1259
+ }
1260
+ const source = catalog.assets.find((asset) => asset.id === dependency.from);
1261
+ if (!source)
1262
+ continue;
1263
+ findings.push({
1264
+ id: "LAYOUT-CONTEXT-REFERENCE-NON_CANONICAL",
1265
+ title: "Declared context path is not under canonical roots",
1266
+ category: "structure",
1267
+ severity: "low",
1268
+ confidence: "medium",
1269
+ evidence: metadataFindingEvidence(source.sourcePath, target),
1270
+ whyItMatters: "Declared context references should resolve through canonical contexts/**, skills/**, or tools/** repo paths.",
1271
+ remediation: "Rewrite declared requires_context or optional_context values to canonical repo-root paths.",
1272
+ verificationSteps: ["Run renma graph and confirm all edges resolve."],
1273
+ });
1274
+ }
1275
+ return findings;
1276
+ }
1277
+ function executableCommands(document) {
1278
+ return document.codeFences.flatMap((fence) => fence.content
1279
+ .split(/\r?\n/)
1280
+ .map((line, index) => ({
1281
+ command: line.trim(),
1282
+ line: fence.startLine + index + 1,
1283
+ }))
1284
+ .filter(({ command }) => /^(node|bash|sh|python|python3)\s+/.test(command)));
1285
+ }
1286
+ function firstExecutableCommand(document) {
1287
+ return executableCommands(document)[0];
1288
+ }
1289
+ function helperScriptPath(command) {
1290
+ const parts = command.split(/\s+/).slice(1);
1291
+ return parts.find((part) => /(?:^|\/)scripts\/.+\.(?:mjs|js|cjs|sh|bash|py)$/.test(part));
1292
+ }
1293
+ function canonicalHelperTarget(config, scriptPath) {
1294
+ const parts = scriptPath.split("/");
1295
+ const skillName = parts[1] ?? "unknown";
1296
+ const rest = parts.slice(3).join("/");
1297
+ const workflow = canonicalWorkflowName(config, skillName);
1298
+ return helperAssetPath(config, workflow, `scripts/${rest}`);
1299
+ }
1300
+ function contextAssetPath(config, workflow, rest) {
1301
+ const namespace = config.layout.toolNamespace;
1302
+ if (namespace)
1303
+ return `contexts/tools/${namespace}/${workflow}/${rest}`;
1304
+ return `contexts/${workflow}/${rest}`;
1305
+ }
1306
+ function helperAssetPath(config, workflow, rest) {
1307
+ const namespace = config.layout.toolNamespace;
1308
+ if (namespace)
1309
+ return `tools/${namespace}/${workflow}/${rest}`;
1310
+ return `tools/${workflow}/${rest}`;
1311
+ }
1312
+ function isCanonicalSkillEntrypoint(pathValue) {
1313
+ return /^skills\/[^/]+\/SKILL\.md$/.test(pathValue);
1314
+ }
1315
+ function repositoryRoot(documents) {
1316
+ const document = documents[0];
1317
+ if (!document)
1318
+ return undefined;
1319
+ return document.artifact.absolutePath.slice(0, document.artifact.absolutePath.length - document.artifact.path.length);
1320
+ }
1321
+ function findingAt(document, id, title, category, severity, line, snippet, remediation, details = {}) {
1322
+ return {
1323
+ ...finding(id, title, category, severity, document, remediation, details),
1324
+ evidence: evidence(document, line, snippet),
1325
+ remediation,
1326
+ };
1327
+ }
1328
+ function lineForOffset(text, offset) {
1329
+ return text.slice(0, offset).split(/\r?\n/).length;
1330
+ }
1331
+ function lineText(document, line) {
1332
+ return document.lines[line - 1] ?? "";
1333
+ }
1334
+ function isRepoPathLike(value) {
1335
+ return /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/.test(value);
1336
+ }
1337
+ function words(text) {
1338
+ return text.match(/[A-Za-z0-9_./+=-]+|[^\sA-Za-z0-9_./+=-]/g) ?? [];
1339
+ }
1340
+ function finding(id, title, category, severity, document, remediation, details = {}) {
1341
+ return {
1342
+ id,
1343
+ title,
1344
+ category,
1345
+ severity,
1346
+ confidence: "high",
1347
+ whyItMatters: details.whyItMatters ??
1348
+ "Skills and repository instructions are loaded into agent context, so risky or unclear text can become risky behavior.",
1349
+ remediation,
1350
+ ...(details.constraints ? { constraints: details.constraints } : {}),
1351
+ ...(details.verificationSteps
1352
+ ? { verificationSteps: details.verificationSteps }
1353
+ : {}),
1354
+ ...(details.llmHint ? { llmHint: details.llmHint } : {}),
1355
+ };
1356
+ }
1357
+ function documentFinding(document, id, title, category, severity, remediation, details = {}) {
1358
+ const firstContentLine = document.lines.findIndex((line) => line.trim().length > 0);
1359
+ const lineNumber = firstContentLine >= 0 ? firstContentLine + 1 : 1;
1360
+ return {
1361
+ id,
1362
+ title,
1363
+ category,
1364
+ severity,
1365
+ confidence: "medium",
1366
+ evidence: evidence(document, lineNumber, document.lines[firstContentLine] ?? ""),
1367
+ whyItMatters: details.whyItMatters ??
1368
+ "Clear skill structure helps agents choose the right workflow and report useful evidence.",
1369
+ remediation,
1370
+ ...(details.constraints ? { constraints: details.constraints } : {}),
1371
+ ...(details.verificationSteps
1372
+ ? { verificationSteps: details.verificationSteps }
1373
+ : {}),
1374
+ ...(details.llmHint ? { llmHint: details.llmHint } : {}),
1375
+ };
1376
+ }
1377
+ function evidence(document, line, snippet) {
1378
+ return {
1379
+ path: document.artifact.path,
1380
+ startLine: line,
1381
+ endLine: line,
1382
+ snippet: snippet.trim().slice(0, 240),
1383
+ };
1384
+ }
1385
+ function localSupportUnreachableRuleId(kind) {
1386
+ if (kind === "profile")
1387
+ return "SUPPORT-UNREACHABLE-PROFILE";
1388
+ if (kind === "example")
1389
+ return "SUPPORT-UNREACHABLE-EXAMPLE";
1390
+ return "SUPPORT-UNREACHABLE-REFERENCE";
1391
+ }
1392
+ function escapeRegExp(value) {
1393
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1394
+ }
1395
+ function approximateTokenCount(text) {
1396
+ const matches = text.match(/[A-Za-z0-9_./+=-]+|[^\sA-Za-z0-9_./+=-]/g);
1397
+ return matches?.length ?? 0;
1398
+ }
1399
+ function isPlaceholder(line) {
1400
+ return /(?:example|placeholder|your_|<[^>]+>|\$\{[^}]+})/i.test(line);
1401
+ }
1402
+ function isSuppressed(line) {
1403
+ return /tool-ignore\s+[A-Z0-9-]+/.test(line);
1404
+ }
1405
+ function hasNearbyConfirmation(lines, matchedLine) {
1406
+ const index = lines.indexOf(matchedLine);
1407
+ const window = lines
1408
+ .slice(Math.max(0, index - 3), Math.min(lines.length, index + 4))
1409
+ .join("\n")
1410
+ .toLowerCase();
1411
+ return /confirm|confirmation|backup|rollback|dry-run|explicit (?:approval|request|requested|permission)|explicitly request(?:s|ed)?/.test(window);
1412
+ }
1413
+ //# sourceMappingURL=rules.js.map