@ploaness/governance 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/access-boundary.d.ts +41 -0
- package/dist/access-boundary.d.ts.map +1 -0
- package/dist/access-boundary.js +26 -0
- package/dist/access-boundary.js.map +1 -0
- package/dist/agent-references.d.ts +12 -0
- package/dist/agent-references.d.ts.map +1 -0
- package/dist/agent-references.js +53 -0
- package/dist/agent-references.js.map +1 -0
- package/dist/architecture-layers.d.ts +11 -0
- package/dist/architecture-layers.d.ts.map +1 -0
- package/dist/architecture-layers.js +47 -0
- package/dist/architecture-layers.js.map +1 -0
- package/dist/asset-policy.d.ts +138 -0
- package/dist/asset-policy.d.ts.map +1 -0
- package/dist/asset-policy.js +312 -0
- package/dist/asset-policy.js.map +1 -0
- package/dist/banned-typography.d.ts +13 -0
- package/dist/banned-typography.d.ts.map +1 -0
- package/dist/banned-typography.js +78 -0
- package/dist/banned-typography.js.map +1 -0
- package/dist/bundle-budget.d.ts +16 -0
- package/dist/bundle-budget.d.ts.map +1 -0
- package/dist/bundle-budget.js +37 -0
- package/dist/bundle-budget.js.map +1 -0
- package/dist/commit-message.d.ts +37 -0
- package/dist/commit-message.d.ts.map +1 -0
- package/dist/commit-message.js +144 -0
- package/dist/commit-message.js.map +1 -0
- package/dist/config-references.d.ts +17 -0
- package/dist/config-references.d.ts.map +1 -0
- package/dist/config-references.js +54 -0
- package/dist/config-references.js.map +1 -0
- package/dist/container-failure.d.ts +40 -0
- package/dist/container-failure.d.ts.map +1 -0
- package/dist/container-failure.js +154 -0
- package/dist/container-failure.js.map +1 -0
- package/dist/container-files.d.ts +36 -0
- package/dist/container-files.d.ts.map +1 -0
- package/dist/container-files.js +74 -0
- package/dist/container-files.js.map +1 -0
- package/dist/container-freshness.d.ts +102 -0
- package/dist/container-freshness.d.ts.map +1 -0
- package/dist/container-freshness.js +129 -0
- package/dist/container-freshness.js.map +1 -0
- package/dist/dependency-freshness.d.ts +129 -0
- package/dist/dependency-freshness.d.ts.map +1 -0
- package/dist/dependency-freshness.js +161 -0
- package/dist/dependency-freshness.js.map +1 -0
- package/dist/doc-blocks.d.ts +13 -0
- package/dist/doc-blocks.d.ts.map +1 -0
- package/dist/doc-blocks.js +75 -0
- package/dist/doc-blocks.js.map +1 -0
- package/dist/document-references.d.ts +44 -0
- package/dist/document-references.d.ts.map +1 -0
- package/dist/document-references.js +94 -0
- package/dist/document-references.js.map +1 -0
- package/dist/editorconfig-policy.d.ts +24 -0
- package/dist/editorconfig-policy.d.ts.map +1 -0
- package/dist/editorconfig-policy.js +106 -0
- package/dist/editorconfig-policy.js.map +1 -0
- package/dist/environment-files.d.ts +42 -0
- package/dist/environment-files.d.ts.map +1 -0
- package/dist/environment-files.js +76 -0
- package/dist/environment-files.js.map +1 -0
- package/dist/file-roles.d.ts +39 -0
- package/dist/file-roles.d.ts.map +1 -0
- package/dist/file-roles.js +82 -0
- package/dist/file-roles.js.map +1 -0
- package/dist/generated-denial.d.ts +42 -0
- package/dist/generated-denial.d.ts.map +1 -0
- package/dist/generated-denial.js +87 -0
- package/dist/generated-denial.js.map +1 -0
- package/dist/history-policy.d.ts +32 -0
- package/dist/history-policy.d.ts.map +1 -0
- package/dist/history-policy.js +50 -0
- package/dist/history-policy.js.map +1 -0
- package/dist/image-assets.d.ts +14 -0
- package/dist/image-assets.d.ts.map +1 -0
- package/dist/image-assets.js +205 -0
- package/dist/image-assets.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/install-policy.d.ts +35 -0
- package/dist/install-policy.d.ts.map +1 -0
- package/dist/install-policy.js +61 -0
- package/dist/install-policy.js.map +1 -0
- package/dist/json-shapes.d.ts +86 -0
- package/dist/json-shapes.d.ts.map +1 -0
- package/dist/json-shapes.js +124 -0
- package/dist/json-shapes.js.map +1 -0
- package/dist/license-allowlist.d.ts +15 -0
- package/dist/license-allowlist.d.ts.map +1 -0
- package/dist/license-allowlist.js +157 -0
- package/dist/license-allowlist.js.map +1 -0
- package/dist/network-policy.d.ts +69 -0
- package/dist/network-policy.d.ts.map +1 -0
- package/dist/network-policy.js +173 -0
- package/dist/network-policy.js.map +1 -0
- package/dist/paired-assets.d.ts +34 -0
- package/dist/paired-assets.d.ts.map +1 -0
- package/dist/paired-assets.js +63 -0
- package/dist/paired-assets.js.map +1 -0
- package/dist/payload-access.d.ts +9 -0
- package/dist/payload-access.d.ts.map +1 -0
- package/dist/payload-access.js +168 -0
- package/dist/payload-access.js.map +1 -0
- package/dist/payload-policy.d.ts +18 -0
- package/dist/payload-policy.d.ts.map +1 -0
- package/dist/payload-policy.js +124 -0
- package/dist/payload-policy.js.map +1 -0
- package/dist/payload-source.d.ts +29 -0
- package/dist/payload-source.d.ts.map +1 -0
- package/dist/payload-source.js +84 -0
- package/dist/payload-source.js.map +1 -0
- package/dist/release-age.d.ts +23 -0
- package/dist/release-age.d.ts.map +1 -0
- package/dist/release-age.js +38 -0
- package/dist/release-age.js.map +1 -0
- package/dist/request-limits.d.ts +39 -0
- package/dist/request-limits.d.ts.map +1 -0
- package/dist/request-limits.js +57 -0
- package/dist/request-limits.js.map +1 -0
- package/dist/run-plan.d.ts +32 -0
- package/dist/run-plan.d.ts.map +1 -0
- package/dist/run-plan.js +43 -0
- package/dist/run-plan.js.map +1 -0
- package/dist/run-policy.d.ts +32 -0
- package/dist/run-policy.d.ts.map +1 -0
- package/dist/run-policy.js +37 -0
- package/dist/run-policy.js.map +1 -0
- package/dist/secret-policy.d.ts +13 -0
- package/dist/secret-policy.d.ts.map +1 -0
- package/dist/secret-policy.js +69 -0
- package/dist/secret-policy.js.map +1 -0
- package/dist/settings-layering.d.ts +45 -0
- package/dist/settings-layering.d.ts.map +1 -0
- package/dist/settings-layering.js +134 -0
- package/dist/settings-layering.js.map +1 -0
- package/dist/settings.d.ts +238 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +378 -0
- package/dist/settings.js.map +1 -0
- package/dist/skill-manifest.d.ts +18 -0
- package/dist/skill-manifest.d.ts.map +1 -0
- package/dist/skill-manifest.js +125 -0
- package/dist/skill-manifest.js.map +1 -0
- package/dist/source-text.d.ts +83 -0
- package/dist/source-text.d.ts.map +1 -0
- package/dist/source-text.js +310 -0
- package/dist/source-text.js.map +1 -0
- package/dist/suppression-ceiling.d.ts +48 -0
- package/dist/suppression-ceiling.d.ts.map +1 -0
- package/dist/suppression-ceiling.js +115 -0
- package/dist/suppression-ceiling.js.map +1 -0
- package/dist/tailwind-tokens.d.ts +16 -0
- package/dist/tailwind-tokens.d.ts.map +1 -0
- package/dist/tailwind-tokens.js +70 -0
- package/dist/tailwind-tokens.js.map +1 -0
- package/dist/text-escapes.d.ts +7 -0
- package/dist/text-escapes.d.ts.map +1 -0
- package/dist/text-escapes.js +14 -0
- package/dist/text-escapes.js.map +1 -0
- package/dist/toolchain-pins.d.ts +42 -0
- package/dist/toolchain-pins.d.ts.map +1 -0
- package/dist/toolchain-pins.js +99 -0
- package/dist/toolchain-pins.js.map +1 -0
- package/dist/url-safety.d.ts +12 -0
- package/dist/url-safety.d.ts.map +1 -0
- package/dist/url-safety.js +58 -0
- package/dist/url-safety.js.map +1 -0
- package/dist/version-policy.d.ts +99 -0
- package/dist/version-policy.d.ts.map +1 -0
- package/dist/version-policy.js +306 -0
- package/dist/version-policy.js.map +1 -0
- package/dist/vulnerability-policy.d.ts +66 -0
- package/dist/vulnerability-policy.d.ts.map +1 -0
- package/dist/vulnerability-policy.js +106 -0
- package/dist/vulnerability-policy.js.map +1 -0
- package/dist/wiring-policy.d.ts +226 -0
- package/dist/wiring-policy.d.ts.map +1 -0
- package/dist/wiring-policy.js +458 -0
- package/dist/wiring-policy.js.map +1 -0
- package/dist/wiring-violation.d.ts +6 -0
- package/dist/wiring-violation.d.ts.map +1 -0
- package/dist/wiring-violation.js +8 -0
- package/dist/wiring-violation.js.map +1 -0
- package/dist/workspace-policy.d.ts +177 -0
- package/dist/workspace-policy.d.ts.map +1 -0
- package/dist/workspace-policy.js +228 -0
- package/dist/workspace-policy.js.map +1 -0
- package/dist/yaml-blocks.d.ts +43 -0
- package/dist/yaml-blocks.d.ts.map +1 -0
- package/dist/yaml-blocks.js +87 -0
- package/dist/yaml-blocks.js.map +1 -0
- package/package.json +41 -0
package/dist/settings.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// Consumer-declared parameters. A project
|
|
2
|
+
// declares them under a `ploaness` key in its package.json. Every field is optional and every default is
|
|
3
|
+
// deliberately strict, so a project that declares nothing still receives the full contract.
|
|
4
|
+
//
|
|
5
|
+
// The set is intentionally small. ploaness owns the rules; a project may declare only the facts ploaness
|
|
6
|
+
// cannot know for it: where its sources live, which generated file a rule must skip, how large its
|
|
7
|
+
// bundle may grow. There is no field that turns a gate off, because "do not disable the failing check"
|
|
8
|
+
// is the contract rather than a preference.
|
|
9
|
+
import { BUNDLE_BUDGET_BYTES } from './bundle-budget.js';
|
|
10
|
+
import { matchesGlob, matchesRole } from './file-roles.js';
|
|
11
|
+
import { GENERATED_ARTEFACTS } from './generated-denial.js';
|
|
12
|
+
// Only string-valued entries survive `asStringRecord`: a non-string would reach `spawn` as a
|
|
13
|
+
// malformed environment.
|
|
14
|
+
import { asRecord, asStringRecord, asText, isArray, isRecord } from './json-shapes.js';
|
|
15
|
+
// Imported rather than recomputed. The same three lines stood here and in `bundle-budget.ts`, which is
|
|
16
|
+
// the module that owns this number - two arithmetic expressions that had to agree, in a package whose
|
|
17
|
+
// own guidance says a value stated twice will not stay stated twice.
|
|
18
|
+
const DEFAULT_BUNDLE_BUDGET_BYTES = BUNDLE_BUDGET_BYTES;
|
|
19
|
+
// The two variables Payload itself requires of every project. They are defaults rather than a hard-coded
|
|
20
|
+
// environment, so a project whose config demands more can add to them, but no project has to restate what
|
|
21
|
+
// Payload already mandates. Both are inert placeholders: no host is contacted and no secret is signed.
|
|
22
|
+
const DEFAULT_ANALYSIS_ENV = {
|
|
23
|
+
PAYLOAD_SECRET: 'ploaness-static-analysis-placeholder',
|
|
24
|
+
DATABASE_URL: 'postgres://127.0.0.1:5432/ploaness-static-analysis',
|
|
25
|
+
};
|
|
26
|
+
// Generated artefacts every Payload project carries. They are defaults rather than hard-coded skips, so
|
|
27
|
+
// a project that renames them can declare its own, but no project has to restate the obvious.
|
|
28
|
+
// The scaffolding Payload generates. A project declaring nothing is held to exactly this, which is the
|
|
29
|
+
// set that shipped before the role was declarable.
|
|
30
|
+
const DEFAULT_FRAMEWORK_GLUE = ['src/app/(payload)/**', 'src/payload.config.ts'];
|
|
31
|
+
/** The pure-logic floor a Payload project has by convention. */
|
|
32
|
+
const DEFAULT_PURE_LOGIC_ROOTS = ['src/access', 'src/lib'];
|
|
33
|
+
// `(^|/)` rather than `^`: these are ploaness's own patterns, and they name files RELATIVE TO A MEMBER.
|
|
34
|
+
// The tracked-tree gates walk the whole repository, so anchoring at the repository root would exempt
|
|
35
|
+
// `src/payload-types.ts` in a single-package project and miss `apps/web/src/payload-types.ts` in a
|
|
36
|
+
// workspace - reporting a generated file as a typography defect in one shape and not the other.
|
|
37
|
+
const MEMBER_RELATIVE = '(^|/)';
|
|
38
|
+
const DEFAULT_TYPOGRAPHY_EXCLUSIONS = [
|
|
39
|
+
String.raw `${MEMBER_RELATIVE}\.claude/`,
|
|
40
|
+
String.raw `${MEMBER_RELATIVE}src/payload-types\.ts$`,
|
|
41
|
+
String.raw `${MEMBER_RELATIVE}src/payload-generated-schema\.ts$`,
|
|
42
|
+
String.raw `importMap\.js$`,
|
|
43
|
+
];
|
|
44
|
+
const DEFAULT_JAVASCRIPT_ALLOWLIST = [
|
|
45
|
+
String.raw `${MEMBER_RELATIVE}eslint\.config\.mjs$`,
|
|
46
|
+
String.raw `importMap\.js$`,
|
|
47
|
+
// ploaness admits Tailwind deliberately - the shipped stylelint configuration lists its at-rules as a
|
|
48
|
+
// CSS dialect a project may legitimately be written in. Tailwind v4 needs a PostCSS configuration, and
|
|
49
|
+
// Next reads it only as `.mjs` or `.js`. Anticipating the dialect in one gate while banning the file
|
|
50
|
+
// it requires in another left a project no move but an allowlist entry claiming a role it does not
|
|
51
|
+
// have: the file is not hand-written JavaScript by choice.
|
|
52
|
+
String.raw `${MEMBER_RELATIVE}postcss\.config\.(mjs|js)$`,
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* The directories a project holds first-party source in unless it says otherwise.
|
|
56
|
+
*
|
|
57
|
+
* Exported because `config-references.ts` built its own alternation out of the same names, and a
|
|
58
|
+
* project that declared an extra source root got its config carve-outs left unchecked while the two
|
|
59
|
+
* lists drifted apart in silence.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_SOURCE_ROOTS = ['src', 'tests', 'scripts'];
|
|
62
|
+
const DEFAULT_SERVER_URL = 'http://localhost:3000';
|
|
63
|
+
// Payload's admin panel and REST API. Neither is a public page, both are reachable from a link on one,
|
|
64
|
+
// and axe run against the admin bundle judges Payload's markup rather than the project's. A project
|
|
65
|
+
// that moved either route adds its own; no project may take these away.
|
|
66
|
+
const DEFAULT_SKIPPED_ROUTES = ['/admin', '/api'];
|
|
67
|
+
// A CMS exists so that people who do not commit can publish pages, so the route count grows without any
|
|
68
|
+
// change to the repository. A cap set where a small site sits therefore fails on an editor's ordinary
|
|
69
|
+
// Tuesday, on a site that is perfectly accessible, and the only valve a project holds is the skip list -
|
|
70
|
+
// which means excusing real public pages from accessibility checking so that the accessibility check
|
|
71
|
+
// passes. That is the wrong trade to leave a team, so the ceiling is set where a growing site lives and
|
|
72
|
+
// the sweep's own clock is bounded separately.
|
|
73
|
+
const DEFAULT_ACCESSIBILITY_ROUTE_BUDGET = 250;
|
|
74
|
+
// Roles that carry no unit-test seam in any Payload application: framework glue verified end to end,
|
|
75
|
+
// generated files, and operational data scripts. A project adds to this; it never shrinks it.
|
|
76
|
+
/**
|
|
77
|
+
* The files the coverage report measures. Declared here rather than in the Vitest config because two
|
|
78
|
+
* things need to agree about it: the config that produces the report, and the rule that judges whether
|
|
79
|
+
* a declared exclusion reaches anything the report measured. Two literals that must stay equal will not.
|
|
80
|
+
*/
|
|
81
|
+
export const COVERAGE_INCLUDE = ['src/**/*.ts', 'scripts/**/*.ts'];
|
|
82
|
+
const DEFAULT_COVERAGE_EXCLUDE = [
|
|
83
|
+
'src/app/**',
|
|
84
|
+
'src/payload.config.ts',
|
|
85
|
+
'src/payload-types.ts',
|
|
86
|
+
'src/seed/**',
|
|
87
|
+
'src/**/*.d.ts',
|
|
88
|
+
'src/**/*.tsx',
|
|
89
|
+
// Seeded by ploaness rather than written by the project, and verified end to end by
|
|
90
|
+
// `security-headers.e2e.spec.ts` - an asset ploaness also seeds, and which shipping made the
|
|
91
|
+
// end-to-end suite mandatory. Without this line every consumer meets a coverage failure on day one,
|
|
92
|
+
// on a file it did not author, and answers it by restating the same exclusion in its own manifest.
|
|
93
|
+
// A unit test would assert that the policy builder returns the string it returns; the sweep asks the
|
|
94
|
+
// running application what it actually sent, which is the question worth failing on.
|
|
95
|
+
'src/proxy.ts',
|
|
96
|
+
];
|
|
97
|
+
const isTextArray = (raw) => isArray(raw) && raw.every((entry) => typeof entry === 'string');
|
|
98
|
+
const asStringArray = (raw, fallback) => isTextArray(raw) ? raw : fallback;
|
|
99
|
+
const asPositiveInteger = (raw, fallback) => typeof raw === 'number' && Number.isSafeInteger(raw) && raw > 0 ? raw : fallback;
|
|
100
|
+
// Zero is a meaningful value here, not a missing one: "no suppression is permitted" is a position a
|
|
101
|
+
// project may take, so this cannot reuse asPositiveInteger. Undefined means the project declares no
|
|
102
|
+
// cap of its own and the earned ceiling applies.
|
|
103
|
+
const asNonNegativeInteger = (raw) => typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0 ? raw : undefined;
|
|
104
|
+
// An entry without a non-empty reason is dropped rather than honoured: taking over a managed path is a
|
|
105
|
+
// decision the project must record, so an unexplained entry must not weaken the asset gate.
|
|
106
|
+
const asUnmanagedAssets = (raw) => isArray(raw)
|
|
107
|
+
? raw.flatMap((entry) => {
|
|
108
|
+
const record = asRecord(entry);
|
|
109
|
+
const path = asText(record['path']);
|
|
110
|
+
const reason = asText(record['reason']);
|
|
111
|
+
return path.length > 0 && reason.trim().length > 0 ? [{ path, reason }] : [];
|
|
112
|
+
})
|
|
113
|
+
: [];
|
|
114
|
+
// Both halves are load-bearing: a command with no url gives Playwright nothing to wait for, and a url
|
|
115
|
+
// with no command names a server nobody starts. An incomplete entry is dropped rather than half-honoured,
|
|
116
|
+
// because starting a process the suite then cannot reach is the one outcome worse than not starting it.
|
|
117
|
+
const asAuxiliaryServers = (raw) => isArray(raw)
|
|
118
|
+
? raw.flatMap((entry) => {
|
|
119
|
+
const record = asRecord(entry);
|
|
120
|
+
const command = asText(record['command']).trim();
|
|
121
|
+
const url = asText(record['url']).trim();
|
|
122
|
+
if (command.length === 0 || url.length === 0) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
// Optional, unlike the two above: a server started in the package under test needs no
|
|
126
|
+
// directory, and only a sibling application does.
|
|
127
|
+
const cwd = asText(record['cwd']).trim();
|
|
128
|
+
return [cwd.length > 0 ? { command, url, cwd } : { command, url }];
|
|
129
|
+
})
|
|
130
|
+
: [];
|
|
131
|
+
// An advisory date must be recorded as a date. An entry whose reason or date is missing or malformed is
|
|
132
|
+
// dropped rather than honoured, so a typo re-exposes the finding instead of quietly excusing it.
|
|
133
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
134
|
+
const asVulnerabilityAllowlist = (raw) => isArray(raw)
|
|
135
|
+
? raw.flatMap((entry) => {
|
|
136
|
+
const record = asRecord(entry);
|
|
137
|
+
const advisory = asText(record['advisory']).trim();
|
|
138
|
+
const reason = asText(record['reason']).trim();
|
|
139
|
+
const addedOn = asText(record['addedOn']);
|
|
140
|
+
const isRecorded = advisory.length > 0 && reason.length > 0 && ISO_DATE.test(addedOn);
|
|
141
|
+
return isRecorded ? [{ advisory, reason, addedOn }] : [];
|
|
142
|
+
})
|
|
143
|
+
: [];
|
|
144
|
+
// An unexplained exception is dropped, exactly as an unexplained managed-path takeover is: a fixture
|
|
145
|
+
// credential is a decision the project must record, and a typo must re-expose the finding rather than
|
|
146
|
+
// quietly widen the scan.
|
|
147
|
+
const asSecretAllowlist = (raw) => isArray(raw)
|
|
148
|
+
? raw.flatMap((entry) => {
|
|
149
|
+
const record = asRecord(entry);
|
|
150
|
+
const filePath = asText(record['path']).trim();
|
|
151
|
+
const reason = asText(record['reason']).trim();
|
|
152
|
+
return filePath.length > 0 && reason.length > 0 ? [{ path: filePath, reason }] : [];
|
|
153
|
+
})
|
|
154
|
+
: [];
|
|
155
|
+
// Dropped on any missing field, for the reason an unexplained secret exception is dropped: a granted
|
|
156
|
+
// anonymous permission is a decision the project must record, and a typo must re-expose the finding
|
|
157
|
+
// rather than quietly excuse a permission nobody chose.
|
|
158
|
+
const asPublicAccess = (raw) => isArray(raw)
|
|
159
|
+
? raw.flatMap((entry) => {
|
|
160
|
+
const record = asRecord(entry);
|
|
161
|
+
const entity = asText(record['entity']).trim();
|
|
162
|
+
const operation = asText(record['operation']).trim();
|
|
163
|
+
const reason = asText(record['reason']).trim();
|
|
164
|
+
return entity.length > 0 && operation.length > 0 && reason.length > 0
|
|
165
|
+
? [{ entity, operation, reason }]
|
|
166
|
+
: [];
|
|
167
|
+
})
|
|
168
|
+
: [];
|
|
169
|
+
// An exclusion narrows a gate's scope, which is the one thing the standard says a project's settings
|
|
170
|
+
// may not do without the harness's leave. The leave it grants is an exclusion by file role - so an
|
|
171
|
+
// entry states the role it is claiming, and an entry that states none is dropped rather than honoured.
|
|
172
|
+
// A dropped entry makes the gate stricter, which is the safe direction to fail in.
|
|
173
|
+
const asDeclaredExclusions = (raw, setting, kind) => isArray(raw)
|
|
174
|
+
? raw.map((entry) => readExclusion(entry, setting, kind))
|
|
175
|
+
: [];
|
|
176
|
+
// A bare string is kept with an empty reason rather than dropped silently, so the gate can name the
|
|
177
|
+
// entry the project wrote instead of reporting that its exclusions simply stopped applying.
|
|
178
|
+
const readExclusion = (entry, setting, kind) => {
|
|
179
|
+
if (typeof entry === 'string') {
|
|
180
|
+
return { setting, pattern: entry, reason: '', kind };
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(entry)) {
|
|
183
|
+
return { setting, pattern: '', reason: '', kind };
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
setting,
|
|
187
|
+
pattern: asText(entry['pattern']),
|
|
188
|
+
reason: asText(entry['reason']).trim(),
|
|
189
|
+
kind,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
const honoured = (entries) => entries
|
|
193
|
+
.filter((entry) => entry.pattern.length > 0 && entry.reason.length > 0)
|
|
194
|
+
.map((entry) => entry.pattern);
|
|
195
|
+
// The kind travels with the setting because this is the only place the answer is not a guess: by the
|
|
196
|
+
// time a rule holds the entry, `^\.vale/styles/` and `src/app/**` are two strings that look alike.
|
|
197
|
+
const readDeclaredLists = (raw) => ({
|
|
198
|
+
typography: asDeclaredExclusions(raw['typographyExclusions'], 'typographyExclusions', 'regex'),
|
|
199
|
+
javascript: asDeclaredExclusions(raw['javascriptAllowlist'], 'javascriptAllowlist', 'regex'),
|
|
200
|
+
coverage: asDeclaredExclusions(raw['coverageExclude'], 'coverageExclude', 'glob'),
|
|
201
|
+
routes: asDeclaredExclusions(raw['accessibilitySkipRoutes'], 'accessibilitySkipRoutes', 'route'),
|
|
202
|
+
glue: asDeclaredExclusions(raw['frameworkGlue'], 'frameworkGlue', 'glob'),
|
|
203
|
+
generated: asDeclaredExclusions(raw['generatedArtefacts'], 'generatedArtefacts', 'glob'),
|
|
204
|
+
layers: asDeclaredExclusions(raw['pureLogicRoots'], 'pureLogicRoots', 'glob'),
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* Read the `ploaness` key of a parsed package.json into a fully defaulted settings object. A malformed
|
|
208
|
+
* value falls back to the strict default rather than failing the read, so a typo can never widen a rule.
|
|
209
|
+
* @param packageJson the parsed package.json of the consuming project.
|
|
210
|
+
* @returns the effective settings, with every field populated.
|
|
211
|
+
*/
|
|
212
|
+
export const readSettings = (packageJson) => readRawSettings(ploanessBlock(packageJson));
|
|
213
|
+
/** The raw `ploaness` block of a manifest, before any default is applied. */
|
|
214
|
+
export const ploanessBlock = (packageJson) => asRecord(asRecord(packageJson)['ploaness']);
|
|
215
|
+
// Every numeric ceiling is read this way, and the shared reading IS the rule: a project may hold itself
|
|
216
|
+
// to a smaller number, and can never buy a pass by declaring a bigger one. Stating it once is what stops
|
|
217
|
+
// the second ceiling drifting from the first - the same drift `STRICTEST_NUMBER` guards one level up,
|
|
218
|
+
// where a workspace member could otherwise widen what the root declared.
|
|
219
|
+
const clampedCeiling = (declared, shipped) => Math.min(shipped, asPositiveInteger(declared, shipped));
|
|
220
|
+
/**
|
|
221
|
+
* Read an already-layered raw settings block into a fully defaulted settings object.
|
|
222
|
+
* @param raw the raw block, after any layering.
|
|
223
|
+
* @returns the effective settings, with every field populated.
|
|
224
|
+
*/
|
|
225
|
+
export const readRawSettings = (raw) => {
|
|
226
|
+
const { typography: declaredTypography, javascript: declaredJavascript, coverage: declaredCoverage, routes: declaredRoutes, glue: declaredGlue, generated: declaredGenerated, layers: declaredLayers, } = readDeclaredLists(raw);
|
|
227
|
+
return {
|
|
228
|
+
// Additive, like every other list field. Replacing the default let a project declare `["src"]` and
|
|
229
|
+
// silently drop `tests` and `scripts` from the conventions, payload-rules, suppressions, css and
|
|
230
|
+
// architecture gates - a scope narrowing the harness is supposed to refuse.
|
|
231
|
+
sourceRoots: [
|
|
232
|
+
...new Set([...DEFAULT_SOURCE_ROOTS, ...asStringArray(raw['sourceRoots'], [])]),
|
|
233
|
+
],
|
|
234
|
+
unmanagedAssets: asUnmanagedAssets(raw['unmanagedAssets']),
|
|
235
|
+
typographyExclusions: [...DEFAULT_TYPOGRAPHY_EXCLUSIONS, ...honoured(declaredTypography)],
|
|
236
|
+
frameworkGlue: [...DEFAULT_FRAMEWORK_GLUE, ...honoured(declaredGlue)],
|
|
237
|
+
generatedArtefacts: [...GENERATED_ARTEFACTS, ...honoured(declaredGenerated)],
|
|
238
|
+
pureLogicRoots: [...DEFAULT_PURE_LOGIC_ROOTS, ...layerRoots(declaredLayers)],
|
|
239
|
+
declaredExclusions: [
|
|
240
|
+
...declaredTypography,
|
|
241
|
+
...declaredJavascript,
|
|
242
|
+
...declaredCoverage,
|
|
243
|
+
...declaredRoutes,
|
|
244
|
+
...declaredGlue,
|
|
245
|
+
...declaredGenerated,
|
|
246
|
+
...declaredLayers,
|
|
247
|
+
],
|
|
248
|
+
javascriptAllowlist: [...DEFAULT_JAVASCRIPT_ALLOWLIST, ...honoured(declaredJavascript)],
|
|
249
|
+
coverageExclude: [...DEFAULT_COVERAGE_EXCLUDE, ...honoured(declaredCoverage)],
|
|
250
|
+
bundleBudgetBytes: clampedCeiling(raw['bundleBudgetBytes'], DEFAULT_BUNDLE_BUDGET_BYTES),
|
|
251
|
+
maxSuppressions: asNonNegativeInteger(raw['maxSuppressions']),
|
|
252
|
+
vulnerabilitySeverity: typeof raw['vulnerabilitySeverity'] === 'string' ? raw['vulnerabilitySeverity'] : undefined,
|
|
253
|
+
vulnerabilityAllowlist: asVulnerabilityAllowlist(raw['vulnerabilityAllowlist']),
|
|
254
|
+
secretAllowlist: asSecretAllowlist(raw['secretAllowlist']),
|
|
255
|
+
publicAccess: asPublicAccess(raw['publicAccess']),
|
|
256
|
+
pretest: asStringArray(raw['pretest'], []),
|
|
257
|
+
testWrapper: asStringArray(raw['testWrapper'], []),
|
|
258
|
+
serverUrl: typeof raw['serverUrl'] === 'string' ? raw['serverUrl'] : DEFAULT_SERVER_URL,
|
|
259
|
+
auxiliaryServers: asAuxiliaryServers(raw['auxiliaryServers']),
|
|
260
|
+
accessibilitySkipRoutes: [...DEFAULT_SKIPPED_ROUTES, ...honoured(declaredRoutes)],
|
|
261
|
+
accessibilityRouteBudget: clampedCeiling(raw['accessibilityRouteBudget'], DEFAULT_ACCESSIBILITY_ROUTE_BUDGET),
|
|
262
|
+
analysisEnv: { ...DEFAULT_ANALYSIS_ENV, ...asStringRecord(raw['analysisEnv']) },
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Report every declared exclusion the harness cannot honour.
|
|
267
|
+
*
|
|
268
|
+
* This once also refused an entry whose pattern named a single existing file, on the reading that one
|
|
269
|
+
* path is not a role. The governing standard now judges an exclusion by what it REACHES rather than by
|
|
270
|
+
* how specific it looks, and `findUnreachedExclusions` below is that rule. A named file the report
|
|
271
|
+
* measured excludes something; the defect the standard names is an entry that excludes nothing.
|
|
272
|
+
* @param entries the exclusions the project declared.
|
|
273
|
+
* @returns one message per entry that states no role.
|
|
274
|
+
*/
|
|
275
|
+
export const findConvenienceExclusions = (entries) => entries.flatMap((entry) => entry.reason.length === 0
|
|
276
|
+
? [
|
|
277
|
+
`${entry.setting} entry "${entry.pattern}" states no reason; ` +
|
|
278
|
+
'an exclusion is granted by file role, so the role must be written down',
|
|
279
|
+
]
|
|
280
|
+
: []);
|
|
281
|
+
/**
|
|
282
|
+
* Report every declared exclusion that matches none of the paths it could have excluded.
|
|
283
|
+
*
|
|
284
|
+
* An exclusion that reaches nothing leaves the report reading exactly as it would have read without it,
|
|
285
|
+
* so it records a decision nobody can see the effect of - and it outlives the file it was written for,
|
|
286
|
+
* which is how a stale carve-out survives the rename that made it meaningless. A route exclusion is
|
|
287
|
+
* skipped: it names a URL the crawl must not follow, and no path in the repository can confirm it.
|
|
288
|
+
* @param entries the exclusions the project declared.
|
|
289
|
+
* @param candidates the repo-relative paths the corresponding gate would otherwise have judged.
|
|
290
|
+
* @returns one message per entry that reaches nothing.
|
|
291
|
+
*/
|
|
292
|
+
export const findUnreachedExclusions = (entries, candidates) => entries
|
|
293
|
+
.filter((entry) => entry.kind !== 'route' && entry.pattern.length > 0 && entry.reason.length > 0)
|
|
294
|
+
.filter((entry) => !reachesAny(entry, candidates))
|
|
295
|
+
.map((entry) => `${entry.setting} entry "${entry.pattern}" excludes nothing; ` +
|
|
296
|
+
'it matches no file the gate would have judged, so it records a decision with no effect');
|
|
297
|
+
const reachesAny = (entry, candidates) => candidates.some((candidate) => entry.kind === 'glob'
|
|
298
|
+
? matchesGlob(entry.pattern, candidate)
|
|
299
|
+
: matchesRole(candidate, [entry.pattern]));
|
|
300
|
+
/**
|
|
301
|
+
* The setting whose patterns are matched against the files the coverage report measures, rather than
|
|
302
|
+
* against every tracked file.
|
|
303
|
+
*/
|
|
304
|
+
const COVERAGE_SETTING = 'coverageExclude';
|
|
305
|
+
/**
|
|
306
|
+
* A directory path without its trailing slashes.
|
|
307
|
+
*
|
|
308
|
+
* Written as a recursion rather than as `/\/+$/`, which backtracks super-linearly on a pathological
|
|
309
|
+
* input - a rule this repository holds itself to.
|
|
310
|
+
* @param value the declared path.
|
|
311
|
+
* @returns the same path with every trailing slash removed.
|
|
312
|
+
*/
|
|
313
|
+
export const withoutTrailingSlash = (value) => value.endsWith('/') ? withoutTrailingSlash(value.slice(0, -1)) : value;
|
|
314
|
+
/** The floor a project declared, normalised: the renderer appends the slash, so a written one goes. */
|
|
315
|
+
const layerRoots = (declared) => honoured(declared).map((root) => withoutTrailingSlash(root));
|
|
316
|
+
/**
|
|
317
|
+
* The settings whose patterns name a DIRECTORY rather than a file.
|
|
318
|
+
*
|
|
319
|
+
* `pureLogicRoots` names the directories forming the architecture floor. Matched against tracked FILE
|
|
320
|
+
* paths it can never reach anything, so a project declaring a floor that exists was told its declaration
|
|
321
|
+
* excluded nothing - while the only pattern that satisfied that check, `src/config/**`, is spliced into
|
|
322
|
+
* a regex by `pureLogicRule` and produces `^(src/config/**\/)`, which is not a valid regular expression.
|
|
323
|
+
* Between them the two rules left the setting with no legal value at all.
|
|
324
|
+
*/
|
|
325
|
+
const DIRECTORY_SETTINGS = new Set(['pureLogicRoots']);
|
|
326
|
+
/**
|
|
327
|
+
* Every directory that holds a tracked file, at every depth.
|
|
328
|
+
*
|
|
329
|
+
* Derived from the file list rather than read from disk, so the answer covers exactly what is committed:
|
|
330
|
+
* a directory that exists only in a working tree is not part of the repository a gate judges.
|
|
331
|
+
* @param tracked the repo-relative paths of the tracked files.
|
|
332
|
+
* @returns each containing directory once, without a trailing slash.
|
|
333
|
+
*/
|
|
334
|
+
export const trackedDirectories = (tracked) => [
|
|
335
|
+
...new Set(tracked.flatMap((file) => {
|
|
336
|
+
const segments = file.split('/').slice(0, -1);
|
|
337
|
+
return segments.map((_, index) => segments.slice(0, index + 1).join('/'));
|
|
338
|
+
})),
|
|
339
|
+
];
|
|
340
|
+
const measuredByCoverage = (tracked) => tracked.filter((file) => COVERAGE_INCLUDE.some((pattern) => matchesGlob(pattern, file)));
|
|
341
|
+
/**
|
|
342
|
+
* Which paths a setting's exclusions have to reach.
|
|
343
|
+
*
|
|
344
|
+
* The file set is decided by the SETTING an entry came from, never by how its pattern is written.
|
|
345
|
+
* Partitioning on the pattern's shape was right while `coverageExclude` was the only glob-shaped
|
|
346
|
+
* setting, and wrong the moment a second one arrived: it asks a coverage carve-out whether a generated
|
|
347
|
+
* artefact appears in the coverage report, which is precisely where it does not, and it asks a
|
|
348
|
+
* directory declaration to name a file.
|
|
349
|
+
*/
|
|
350
|
+
const candidatesFor = (setting, tracked) => {
|
|
351
|
+
if (setting === COVERAGE_SETTING) {
|
|
352
|
+
return measuredByCoverage(tracked);
|
|
353
|
+
}
|
|
354
|
+
return DIRECTORY_SETTINGS.has(setting) ? trackedDirectories(tracked) : tracked;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Report every declared exclusion that reaches none of the paths ITS OWN setting could have excluded.
|
|
358
|
+
*
|
|
359
|
+
* A composition over `findUnreachedExclusions`: it groups the entries by setting and hands each group
|
|
360
|
+
* the candidate set that setting governs, so a caller supplies the tracked file list and nothing else.
|
|
361
|
+
* @param entries the exclusions the project declared, across every setting.
|
|
362
|
+
* @param tracked the repo-relative paths of the tracked files.
|
|
363
|
+
* @returns one message per entry that reaches nothing.
|
|
364
|
+
*/
|
|
365
|
+
export const findUnreachedExclusionsBySetting = (entries, tracked) => [...new Set(entries.map((entry) => entry.setting))].flatMap((setting) => findUnreachedExclusions(entries
|
|
366
|
+
.filter((entry) => entry.setting === setting)
|
|
367
|
+
.map((entry) => normalised(entry)), candidatesFor(setting, tracked)));
|
|
368
|
+
/**
|
|
369
|
+
* A directory setting tolerates a trailing slash, because the renderer that consumes it appends one.
|
|
370
|
+
*
|
|
371
|
+
* Normalising here rather than in each consumer is what keeps the two from disagreeing: `pureLogicRule`
|
|
372
|
+
* already accepted `src/lib/` and this rule already reported it, which is the same contradiction the
|
|
373
|
+
* partition above exists to remove, one size smaller.
|
|
374
|
+
*/
|
|
375
|
+
const normalised = (entry) => DIRECTORY_SETTINGS.has(entry.setting)
|
|
376
|
+
? { ...entry, pattern: withoutTrailingSlash(entry.pattern) }
|
|
377
|
+
: entry;
|
|
378
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,yGAAyG;AACzG,4FAA4F;AAC5F,EAAE;AACF,yGAAyG;AACzG,mGAAmG;AACnG,uGAAuG;AACvG,4CAA4C;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,6FAA6F;AAC7F,yBAAyB;AACzB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAqKtF,uGAAuG;AACvG,sGAAsG;AACtG,qEAAqE;AACrE,MAAM,2BAA2B,GAAW,mBAAmB,CAAA;AAE/D,yGAAyG;AACzG,0GAA0G;AAC1G,uGAAuG;AACvG,MAAM,oBAAoB,GAAqC;IAC7D,cAAc,EAAE,sCAAsC;IACtD,YAAY,EAAE,oDAAoD;CACnE,CAAA;AAED,wGAAwG;AACxG,8FAA8F;AAC9F,uGAAuG;AACvG,mDAAmD;AACnD,MAAM,sBAAsB,GAAsB,CAAC,sBAAsB,EAAE,uBAAuB,CAAC,CAAA;AAEnG,gEAAgE;AAChE,MAAM,wBAAwB,GAAsB,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;AAE7E,wGAAwG;AACxG,qGAAqG;AACrG,mGAAmG;AACnG,gGAAgG;AAChG,MAAM,eAAe,GAAW,OAAO,CAAA;AAEvC,MAAM,6BAA6B,GAAsB;IACvD,MAAM,CAAC,GAAG,CAAA,GAAG,eAAe,WAAW;IACvC,MAAM,CAAC,GAAG,CAAA,GAAG,eAAe,wBAAwB;IACpD,MAAM,CAAC,GAAG,CAAA,GAAG,eAAe,mCAAmC;IAC/D,MAAM,CAAC,GAAG,CAAA,gBAAgB;CAC3B,CAAA;AAED,MAAM,4BAA4B,GAAsB;IACtD,MAAM,CAAC,GAAG,CAAA,GAAG,eAAe,sBAAsB;IAClD,MAAM,CAAC,GAAG,CAAA,gBAAgB;IAC1B,sGAAsG;IACtG,uGAAuG;IACvG,qGAAqG;IACrG,mGAAmG;IACnG,2DAA2D;IAC3D,MAAM,CAAC,GAAG,CAAA,GAAG,eAAe,4BAA4B;CACzD,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;AAElF,MAAM,kBAAkB,GAAW,uBAAuB,CAAA;AAE1D,uGAAuG;AACvG,oGAAoG;AACpG,wEAAwE;AACxE,MAAM,sBAAsB,GAAsB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAEpE,wGAAwG;AACxG,sGAAsG;AACtG,yGAAyG;AACzG,qGAAqG;AACrG,wGAAwG;AACxG,+CAA+C;AAC/C,MAAM,kCAAkC,GAAW,GAAG,CAAA;AAEtD,qGAAqG;AACrG,8FAA8F;AAC9F;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAsB,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAA;AAErF,MAAM,wBAAwB,GAAsB;IAClD,YAAY;IACZ,uBAAuB;IACvB,sBAAsB;IACtB,aAAa;IACb,eAAe;IACf,cAAc;IACd,oFAAoF;IACpF,6FAA6F;IAC7F,oGAAoG;IACpG,mGAAmG;IACnG,qGAAqG;IACrG,qFAAqF;IACrF,cAAc;CACf,CAAA;AAED,MAAM,WAAW,GAAG,CAAC,GAAY,EAA4B,EAAE,CAC7D,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,KAAc,EAAW,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAA;AAEnF,MAAM,aAAa,GAAG,CAAC,GAAY,EAAE,QAA2B,EAAqB,EAAE,CACrF,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAA;AAEnC,MAAM,iBAAiB,GAAG,CAAC,GAAY,EAAE,QAAgB,EAAU,EAAE,CACnE,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAA;AAElF,oGAAoG;AACpG,oGAAoG;AACpG,iDAAiD;AACjD,MAAM,oBAAoB,GAAG,CAAC,GAAY,EAAsB,EAAE,CAChE,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAEpF,uGAAuG;AACvG,4FAA4F;AAC5F,MAAM,iBAAiB,GAAG,CAAC,GAAY,EAA6B,EAAE,CACpE,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAc,EAA6B,EAAE;QACxD,MAAM,MAAM,GAA4B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,IAAI,GAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;QAC3C,MAAM,MAAM,GAAW,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC/C,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC9E,CAAC,CAAC;IACJ,CAAC,CAAC,EAAE,CAAA;AAER,sGAAsG;AACtG,0GAA0G;AAC1G,wGAAwG;AACxG,MAAM,kBAAkB,GAAG,CAAC,GAAY,EAA8B,EAAE,CACtE,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAc,EAA8B,EAAE;QACzD,MAAM,MAAM,GAA4B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,OAAO,GAAW,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACxD,MAAM,GAAG,GAAW,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAChD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7C,OAAO,EAAE,CAAA;QACX,CAAC;QACD,sFAAsF;QACtF,kDAAkD;QAClD,MAAM,GAAG,GAAW,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAChD,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC,CAAC;IACJ,CAAC,CAAC,EAAE,CAAA;AAER,wGAAwG;AACxG,iGAAiG;AACjG,MAAM,QAAQ,GAAW,qBAAqB,CAAA;AAE9C,MAAM,wBAAwB,GAAG,CAAC,GAAY,EAAqC,EAAE,CACnF,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAc,EAAqC,EAAE;QAChE,MAAM,MAAM,GAA4B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,QAAQ,GAAW,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC1D,MAAM,MAAM,GAAW,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,MAAM,OAAO,GAAW,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;QACjD,MAAM,UAAU,GACd,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACpE,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1D,CAAC,CAAC;IACJ,CAAC,CAAC,EAAE,CAAA;AAER,qGAAqG;AACrG,sGAAsG;AACtG,0BAA0B;AAC1B,MAAM,iBAAiB,GAAG,CAAC,GAAY,EAA8B,EAAE,CACrE,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAc,EAA8B,EAAE;QACzD,MAAM,MAAM,GAA4B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,QAAQ,GAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,MAAM,MAAM,GAAW,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACrF,CAAC,CAAC;IACJ,CAAC,CAAC,EAAE,CAAA;AAER,qGAAqG;AACrG,oGAAoG;AACpG,wDAAwD;AACxD,MAAM,cAAc,GAAG,CAAC,GAAY,EAA2B,EAAE,CAC/D,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,KAAc,EAA2B,EAAE;QACtD,MAAM,MAAM,GAA4B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,MAAM,GAAW,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,MAAM,SAAS,GAAW,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAC5D,MAAM,MAAM,GAAW,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QACtD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YACnE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;YACjC,CAAC,CAAC,EAAE,CAAA;IACR,CAAC,CAAC;IACJ,CAAC,CAAC,EAAE,CAAA;AAER,qGAAqG;AACrG,mGAAmG;AACnG,uGAAuG;AACvG,mFAAmF;AACnF,MAAM,oBAAoB,GAAG,CAC3B,GAAY,EACZ,OAAe,EACf,IAAmB,EACW,EAAE,CAChC,OAAO,CAAC,GAAG,CAAC;IACV,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAc,EAAqB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACrF,CAAC,CAAC,EAAE,CAAA;AAER,oGAAoG;AACpG,4FAA4F;AAC5F,MAAM,aAAa,GAAG,CAAC,KAAc,EAAE,OAAe,EAAE,IAAmB,EAAqB,EAAE;IAChG,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACtD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACnD,CAAC;IACD,OAAO;QACL,OAAO;QACP,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACjC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE;QACtC,IAAI;KACL,CAAA;AACH,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,OAAqC,EAAqB,EAAE,CAC5E,OAAO;KACJ,MAAM,CACL,CAAC,KAAwB,EAAW,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAC3F;KACA,GAAG,CAAC,CAAC,KAAwB,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAa7D,qGAAqG;AACrG,mGAAmG;AACnG,MAAM,iBAAiB,GAAG,CAAC,GAA4B,EAAiB,EAAE,CAAC,CAAC;IAC1E,UAAU,EAAE,oBAAoB,CAAC,GAAG,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,EAAE,OAAO,CAAC;IAC9F,UAAU,EAAE,oBAAoB,CAAC,GAAG,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,EAAE,OAAO,CAAC;IAC5F,QAAQ,EAAE,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC;IACjF,MAAM,EAAE,oBAAoB,CAAC,GAAG,CAAC,yBAAyB,CAAC,EAAE,yBAAyB,EAAE,OAAO,CAAC;IAChG,IAAI,EAAE,oBAAoB,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC;IACzE,SAAS,EAAE,oBAAoB,CAAC,GAAG,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,EAAE,MAAM,CAAC;IACxF,MAAM,EAAE,oBAAoB,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC;CAC9E,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAoB,EAAY,EAAE,CAC7D,eAAe,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;AAE7C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,WAAoB,EAA2B,EAAE,CAC7E,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;AAE7C,wGAAwG;AACxG,yGAAyG;AACzG,sGAAsG;AACtG,yEAAyE;AACzE,MAAM,cAAc,GAAG,CAAC,QAAiB,EAAE,OAAe,EAAU,EAAE,CACpE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAA;AAEzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAA4B,EAAY,EAAE;IACxE,MAAM,EACJ,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,cAAc,GACvB,GAAkB,iBAAiB,CAAC,GAAG,CAAC,CAAA;IACzC,OAAO;QACL,mGAAmG;QACnG,iGAAiG;QACjG,4EAA4E;QAC5E,WAAW,EAAE;YACX,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,oBAAoB,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;SACxF;QACD,eAAe,EAAE,iBAAiB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC1D,oBAAoB,EAAE,CAAC,GAAG,6BAA6B,EAAE,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QACzF,aAAa,EAAE,CAAC,GAAG,sBAAsB,EAAE,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;QACrE,kBAAkB,EAAE,CAAC,GAAG,mBAAmB,EAAE,GAAG,QAAQ,CAAC,iBAAiB,CAAC,CAAC;QAC5E,cAAc,EAAE,CAAC,GAAG,wBAAwB,EAAE,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;QAC5E,kBAAkB,EAAE;YAClB,GAAG,kBAAkB;YACrB,GAAG,kBAAkB;YACrB,GAAG,gBAAgB;YACnB,GAAG,cAAc;YACjB,GAAG,YAAY;YACf,GAAG,iBAAiB;YACpB,GAAG,cAAc;SAClB;QACD,mBAAmB,EAAE,CAAC,GAAG,4BAA4B,EAAE,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QACvF,eAAe,EAAE,CAAC,GAAG,wBAAwB,EAAE,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;QAC7E,iBAAiB,EAAE,cAAc,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;QACxF,eAAe,EAAE,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC7D,qBAAqB,EACnB,OAAO,GAAG,CAAC,uBAAuB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,SAAS;QAC7F,sBAAsB,EAAE,wBAAwB,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAC/E,eAAe,EAAE,iBAAiB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;QAC1D,YAAY,EAAE,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACjD,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;QAC1C,WAAW,EAAE,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;QAClD,SAAS,EAAE,OAAO,GAAG,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,kBAAkB;QACvF,gBAAgB,EAAE,kBAAkB,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC7D,uBAAuB,EAAE,CAAC,GAAG,sBAAsB,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QACjF,wBAAwB,EAAE,cAAc,CACtC,GAAG,CAAC,0BAA0B,CAAC,EAC/B,kCAAkC,CACnC;QACD,WAAW,EAAE,EAAE,GAAG,oBAAoB,EAAE,GAAG,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE;KAChF,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,OAAqC,EAClB,EAAE,CACrB,OAAO,CAAC,OAAO,CAAC,CAAC,KAAwB,EAAqB,EAAE,CAC9D,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;IACvB,CAAC,CAAC;QACE,GAAG,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC,OAAO,sBAAsB;YAC5D,wEAAwE;KAC3E;IACH,CAAC,CAAC,EAAE,CACP,CAAA;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,OAAqC,EACrC,UAA6B,EACV,EAAE,CACrB,OAAO;KACJ,MAAM,CACL,CAAC,KAAwB,EAAW,EAAE,CACpC,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAChF;KACA,MAAM,CAAC,CAAC,KAAwB,EAAW,EAAE,CAAC,CAAC,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;KAC7E,GAAG,CACF,CAAC,KAAwB,EAAU,EAAE,CACnC,GAAG,KAAK,CAAC,OAAO,WAAW,KAAK,CAAC,OAAO,sBAAsB;IAC9D,wFAAwF,CAC3F,CAAA;AAEL,MAAM,UAAU,GAAG,CAAC,KAAwB,EAAE,UAA6B,EAAW,EAAE,CACtF,UAAU,CAAC,IAAI,CAAC,CAAC,SAAiB,EAAW,EAAE,CAC7C,KAAK,CAAC,IAAI,KAAK,MAAM;IACnB,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;IACvC,CAAC,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAC5C,CAAA;AAEH;;;GAGG;AACH,MAAM,gBAAgB,GAAW,iBAAiB,CAAA;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,KAAa,EAAU,EAAE,CAC5D,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AAExE,uGAAuG;AACvG,MAAM,UAAU,GAAG,CAAC,QAAsC,EAAqB,EAAE,CAC/E,QAAQ,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAY,EAAU,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAA;AAE9E;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAS,CAAC,gBAAgB,CAAC,CAAC,CAAA;AAEnF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAA0B,EAAqB,EAAE,CAAC;IACnF,GAAG,IAAI,GAAG,CACR,OAAO,CAAC,OAAO,CAAC,CAAC,IAAY,EAAqB,EAAE;QAClD,MAAM,QAAQ,GAAsB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAChE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,KAAa,EAAU,EAAE,CACvD,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CACvC,CAAA;IACH,CAAC,CAAC,CACH;CACF,CAAA;AAED,MAAM,kBAAkB,GAAG,CAAC,OAA0B,EAAqB,EAAE,CAC3E,OAAO,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CACvC,gBAAgB,CAAC,IAAI,CAAC,CAAC,OAAe,EAAW,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAChF,CAAA;AAEH;;;;;;;;GAQG;AACH,MAAM,aAAa,GAAG,CAAC,OAAe,EAAE,OAA0B,EAAqB,EAAE;IACvF,IAAI,OAAO,KAAK,gBAAgB,EAAE,CAAC;QACjC,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;AAChF,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAC9C,OAAqC,EACrC,OAA0B,EACP,EAAE,CACrB,CAAC,GAAG,IAAI,GAAG,CAAS,OAAO,CAAC,GAAG,CAAC,CAAC,KAAwB,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAC5F,CAAC,OAAe,EAAqB,EAAE,CACrC,uBAAuB,CACrB,OAAO;KACJ,MAAM,CAAC,CAAC,KAAwB,EAAW,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,CAAC;KACxE,GAAG,CAAC,CAAC,KAAwB,EAAqB,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAC1E,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAChC,CACJ,CAAA;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,CAAC,KAAwB,EAAqB,EAAE,CACjE,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;IACnC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,oBAAoB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE;IAC5D,CAAC,CAAC,KAAK,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** A SKILL.md frontmatter defect that would impair the skill's discovery or invocation. */
|
|
2
|
+
export interface SkillViolation {
|
|
3
|
+
readonly rule: 'frontmatter' | 'name' | 'description' | 'keys';
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
}
|
|
6
|
+
/** Inputs for {@link findSkillManifestViolations}: the file's text and its parent directory name. */
|
|
7
|
+
interface SkillManifestInputs {
|
|
8
|
+
readonly content: string;
|
|
9
|
+
readonly directoryName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Return every frontmatter defect in a SKILL.md: missing frontmatter, a missing/non-kebab-case name, a
|
|
13
|
+
* name that does not match its directory, a description that omits a when-to-use clause, or an unknown
|
|
14
|
+
* key. An empty array means the skill's frontmatter contract is sound.
|
|
15
|
+
*/
|
|
16
|
+
export declare const findSkillManifestViolations: (inputs: SkillManifestInputs) => readonly SkillViolation[];
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=skill-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-manifest.d.ts","sourceRoot":"","sources":["../src/skill-manifest.ts"],"names":[],"mappings":"AAQA,2FAA2F;AAC3F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,CAAA;IAC9D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,qGAAqG;AACrG,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AA4HD;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GACtC,QAAQ,mBAAmB,KAC1B,SAAS,cAAc,EAUzB,CAAA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Skill-manifest policy: the pure rules that validate a Claude Code SKILL.md's frontmatter contract.
|
|
2
|
+
// The `skills` gate in packages/cli/src/checks/references.ts walks the filesystem.
|
|
3
|
+
//
|
|
4
|
+
// Scope is deliberately the HIGH-SIGNAL, false-positive-free subset of an agentic-skill linter: the
|
|
5
|
+
// frontmatter Claude Code actually relies on to DISCOVER and INVOKE a skill. Body style (line length,
|
|
6
|
+
// prose-vs-commands, token budget) is intentionally NOT checked - a SKILL.md is a knowledge file with
|
|
7
|
+
// lookup tables and code examples, not a command manifesto, so those rules would reject valid skills.
|
|
8
|
+
// The frontmatter keys Claude Code recognises; anything else is dead metadata that should not accrue.
|
|
9
|
+
// `model`, `argument-hint` and `disable-model-invocation` are as real as the rest: leaving them out
|
|
10
|
+
// reported a valid manifest as carrying an unknown key, which is a rule teaching a project to write a
|
|
11
|
+
// worse file.
|
|
12
|
+
const ALLOWED_KEYS = new Set([
|
|
13
|
+
'name',
|
|
14
|
+
'description',
|
|
15
|
+
'license',
|
|
16
|
+
'allowed-tools',
|
|
17
|
+
'argument-hint',
|
|
18
|
+
'disable-model-invocation',
|
|
19
|
+
'model',
|
|
20
|
+
'metadata',
|
|
21
|
+
]);
|
|
22
|
+
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
23
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---/;
|
|
24
|
+
const TOP_LEVEL_KEY = /^([\w-]+):[ \t]?(.*)$/;
|
|
25
|
+
// A folded or literal block scalar carries its text on the lines beneath the key, indented. Reading
|
|
26
|
+
// only the key's own line yielded ">-" as the description, which then failed every check below it -
|
|
27
|
+
// and a block scalar is simply how a description longer than one line is written.
|
|
28
|
+
const BLOCK_SCALAR = /^[>|][-+]?\d*$/;
|
|
29
|
+
const CONTINUATION = /^\s+\S/;
|
|
30
|
+
// The description must tell the agent WHEN to reach for the skill; a "when ..." clause is that signal.
|
|
31
|
+
// The word is not required to end there: "whenever the user asks" says exactly the same thing, and a
|
|
32
|
+
// trailing word boundary rejected the single most common way of phrasing it.
|
|
33
|
+
const WHEN_TO_USE = /\bwhen/i;
|
|
34
|
+
// Parse only flat, top-level `key: value` lines (column 0). Indented lines under a nested key such as
|
|
35
|
+
// `metadata:` start with whitespace, fail TOP_LEVEL_KEY, and are ignored - so nesting is never flagged.
|
|
36
|
+
// How many of the lines after a block-scalar key belong to it: every indented one, up to the next key.
|
|
37
|
+
const continuationCount = (rest) => {
|
|
38
|
+
const end = rest.findIndex((line) => !CONTINUATION.test(line));
|
|
39
|
+
return end === -1 ? rest.length : end;
|
|
40
|
+
};
|
|
41
|
+
// A key's value, folded together with the indented lines beneath it when the value is a block scalar.
|
|
42
|
+
const valueAt = (lines, index, raw) => {
|
|
43
|
+
if (!BLOCK_SCALAR.test(raw)) {
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
const rest = lines.slice(index + 1);
|
|
47
|
+
return rest
|
|
48
|
+
.slice(0, continuationCount(rest))
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.join(' ');
|
|
51
|
+
};
|
|
52
|
+
const parseFrontmatter = (content) => {
|
|
53
|
+
// Line endings are normalised first. The pattern below is written with `\n`, so a CRLF manifest
|
|
54
|
+
// matched nothing at all and was reported as having no frontmatter - a message that sends the author
|
|
55
|
+
// looking at the three dashes, which were never the problem.
|
|
56
|
+
const match = FRONTMATTER.exec(content.replaceAll('\r\n', '\n'));
|
|
57
|
+
if (match === null) {
|
|
58
|
+
return { present: false, keys: new Map(), keyNames: [] };
|
|
59
|
+
}
|
|
60
|
+
const lines = (match[1] ?? '').split('\n');
|
|
61
|
+
const entries = lines
|
|
62
|
+
.map((line, index) => [
|
|
63
|
+
TOP_LEVEL_KEY.exec(line),
|
|
64
|
+
index,
|
|
65
|
+
])
|
|
66
|
+
.filter((entry) => entry[0] !== null)
|
|
67
|
+
.map((entry) => [
|
|
68
|
+
entry[0][1] ?? '',
|
|
69
|
+
valueAt(lines, entry[1], (entry[0][2] ?? '').trim()).trim(),
|
|
70
|
+
]);
|
|
71
|
+
return {
|
|
72
|
+
present: true,
|
|
73
|
+
keys: new Map(entries),
|
|
74
|
+
keyNames: entries.map((entry) => entry[0]),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const checkName = (name, directoryName) => {
|
|
78
|
+
if (name === undefined) {
|
|
79
|
+
return [{ rule: 'name', reason: 'frontmatter has no "name" key' }];
|
|
80
|
+
}
|
|
81
|
+
const kebab = KEBAB_CASE.test(name)
|
|
82
|
+
? []
|
|
83
|
+
: [{ rule: 'name', reason: `name "${name}" must be kebab-case` }];
|
|
84
|
+
const directoryViolations = name === directoryName
|
|
85
|
+
? []
|
|
86
|
+
: [{ rule: 'name', reason: `name "${name}" must equal parent directory "${directoryName}"` }];
|
|
87
|
+
return [...kebab, ...directoryViolations];
|
|
88
|
+
};
|
|
89
|
+
const checkDescription = (description) => {
|
|
90
|
+
if (description === undefined || description.length === 0) {
|
|
91
|
+
return [{ rule: 'description', reason: 'frontmatter has no "description" key' }];
|
|
92
|
+
}
|
|
93
|
+
if (!WHEN_TO_USE.test(description)) {
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
rule: 'description',
|
|
97
|
+
reason: 'description must say when to use the skill ("when ..." clause)',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
};
|
|
103
|
+
const checkKeys = (keyNames) => keyNames
|
|
104
|
+
.filter((key) => !ALLOWED_KEYS.has(key))
|
|
105
|
+
.map((key) => ({
|
|
106
|
+
rule: 'keys',
|
|
107
|
+
reason: `unknown frontmatter key "${key}"`,
|
|
108
|
+
}));
|
|
109
|
+
/**
|
|
110
|
+
* Return every frontmatter defect in a SKILL.md: missing frontmatter, a missing/non-kebab-case name, a
|
|
111
|
+
* name that does not match its directory, a description that omits a when-to-use clause, or an unknown
|
|
112
|
+
* key. An empty array means the skill's frontmatter contract is sound.
|
|
113
|
+
*/
|
|
114
|
+
export const findSkillManifestViolations = (inputs) => {
|
|
115
|
+
const frontmatter = parseFrontmatter(inputs.content);
|
|
116
|
+
if (!frontmatter.present) {
|
|
117
|
+
return [{ rule: 'frontmatter', reason: 'file must open with valid "---" frontmatter' }];
|
|
118
|
+
}
|
|
119
|
+
return [
|
|
120
|
+
...checkName(frontmatter.keys.get('name'), inputs.directoryName),
|
|
121
|
+
...checkDescription(frontmatter.keys.get('description')),
|
|
122
|
+
...checkKeys(frontmatter.keyNames),
|
|
123
|
+
];
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=skill-manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-manifest.js","sourceRoot":"","sources":["../src/skill-manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** The offset `at` reads to reach the final element. */
|
|
2
|
+
export declare const LAST_CHARACTER: number;
|
|
3
|
+
/** The value `indexOf` and this module's readers return when there is nothing to point at. */
|
|
4
|
+
export declare const NOT_FOUND: number;
|
|
5
|
+
/**
|
|
6
|
+
* The one-based line a source offset falls on.
|
|
7
|
+
* @param source the file contents.
|
|
8
|
+
* @param index the offset.
|
|
9
|
+
* @returns the line number, counting from one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const lineOf: (source: string, index: number) => number;
|
|
12
|
+
/** What a fold step produced, and whether the walk should stop there. */
|
|
13
|
+
export interface Folded<State> {
|
|
14
|
+
readonly state: State;
|
|
15
|
+
readonly stop: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Blank every comment and regular-expression literal to spaces, leaving newlines intact so reported line
|
|
19
|
+
* numbers still point at real source positions. Without this, prose that merely names a banned construct
|
|
20
|
+
* would be reported as the construct itself, which is the difference between a gate that is trusted and
|
|
21
|
+
* one that is worked around.
|
|
22
|
+
* @param source the file contents.
|
|
23
|
+
* @returns the source with comments and regex literals replaced by spaces of equal length.
|
|
24
|
+
*/
|
|
25
|
+
export declare const stripComments: (source: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* The source with every string literal replaced by filler of equal length, on top of what
|
|
28
|
+
* {@link stripComments} erases.
|
|
29
|
+
*
|
|
30
|
+
* A mask, for a caller that must locate syntax without reading inside a string. Offsets still line up
|
|
31
|
+
* with the source, so a position found here names the same character there, and a masked string is
|
|
32
|
+
* still a token rather than a gap. `parseJsonc` is that caller: it removed trailing commas from the
|
|
33
|
+
* document itself, so a comma sitting inside a string before a closing bracket - as a brace-expansion
|
|
34
|
+
* glob ending in one carries - was deleted from the parsed value, and nothing said so.
|
|
35
|
+
* @param source the file contents.
|
|
36
|
+
* @returns the source with comments erased, strings filled, and newlines intact.
|
|
37
|
+
*/
|
|
38
|
+
export declare const maskLiterals: (source: string) => string;
|
|
39
|
+
/** One character of a delimiter-aware scan, with the bracket depth that holds once it is applied. */
|
|
40
|
+
export interface ScanStep {
|
|
41
|
+
readonly character: string;
|
|
42
|
+
readonly index: number;
|
|
43
|
+
readonly depth: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Walk `source` from `start`, tracking string literals and bracket nesting so that a bracket or a quote
|
|
47
|
+
* inside a string cannot unbalance the scan. Every reader below needs exactly this bookkeeping and they
|
|
48
|
+
* differ only in what they do with each character, so it lives here once rather than in each of them.
|
|
49
|
+
* @param source the text to walk.
|
|
50
|
+
* @param start the index to begin at.
|
|
51
|
+
* @param fold called for every character outside a string literal; return stop to end the walk.
|
|
52
|
+
* @param initial the state the fold begins with.
|
|
53
|
+
*/
|
|
54
|
+
export declare const scanDelimited: <State>(source: string, start: number, fold: (state: State, step: ScanStep) => Folded<State>, initial: State) => State;
|
|
55
|
+
/**
|
|
56
|
+
* Read the text between the parenthesis at `open` and its match.
|
|
57
|
+
* @param source the file contents.
|
|
58
|
+
* @param open the index of the opening parenthesis.
|
|
59
|
+
* @returns the argument text, or undefined when the call is unterminated (a parse error another gate reports).
|
|
60
|
+
*/
|
|
61
|
+
export declare const balancedArguments: (source: string, open: number) => string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Reduce an argument list to the characters sitting directly inside its outermost object literal, so a
|
|
64
|
+
* key nested in a `where` clause is not mistaken for a bound declared on the call itself.
|
|
65
|
+
* @param argumentText the text between a call's parentheses.
|
|
66
|
+
* @returns the depth-one characters, with nested structures elided.
|
|
67
|
+
*/
|
|
68
|
+
export declare const topLevelSlice: (argumentText: string) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Every offset at which `needle` occurs literally.
|
|
71
|
+
* @param source the text to search.
|
|
72
|
+
* @param needle the literal to find; its punctuation is not read as pattern syntax.
|
|
73
|
+
* @returns the offsets, in reading order.
|
|
74
|
+
*/
|
|
75
|
+
export declare const occurrences: (source: string, needle: string) => readonly number[];
|
|
76
|
+
/**
|
|
77
|
+
* The keys sitting directly inside the object literal that opens after `index`.
|
|
78
|
+
* @param source the text to read.
|
|
79
|
+
* @param index where to look for the opening brace.
|
|
80
|
+
* @returns the depth-one key names, with every nested structure elided.
|
|
81
|
+
*/
|
|
82
|
+
export declare const topLevelKeys: (source: string, index: number) => readonly string[];
|
|
83
|
+
//# sourceMappingURL=source-text.d.ts.map
|