@starci/eslint-canon-fe 2.0.0 → 3.0.1
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/README.md +38 -92
- package/class-names.mjs +148 -0
- package/file-layout.mjs +6 -7
- package/grammar-boundary.mjs +10 -425
- package/index.mjs +6 -2
- package/landmark.mjs +29 -242
- package/package.json +2 -4
- package/props-and-slots.mjs +148 -452
- package/vendor-boundary.mjs +25 -595
- package/contract.mjs +0 -1387
- package/contracts.ts +0 -312
- package/props.ts +0 -145
package/grammar-boundary.mjs
CHANGED
|
@@ -1,431 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The boundary between product code and `@starci/grammar`.
|
|
3
3
|
*
|
|
4
|
-
* Grammar
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Grammar is a business-neutral HeroUI-backed component package. Product code may compose its
|
|
5
|
+
* components with ordinary React children and typed props; this contribution deliberately does
|
|
6
|
+
* not impose a second rendering protocol or a product-specific adapter lane.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** No product-specific AST restrictions are needed for the neutral Grammar boundary. */
|
|
10
|
+
export const rules = {}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
/** No repository audit is coupled to the neutral Grammar boundary. */
|
|
13
|
+
export const audits = {}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const isBranchOwner = (filename) => filename.includes("/src/components/branches/")
|
|
17
|
-
const isGrammarContract = (filename) => /\/src\/components\/contracts\/grammar\.[^/]+$/.test(filename)
|
|
18
|
-
const isProductSource = (filename) => filename.includes("/src/") && !isTestFile(filename)
|
|
19
|
-
|
|
20
|
-
const jsxName = (node) => node?.name?.type === "JSXIdentifier" ? node.name.name : null
|
|
21
|
-
|
|
22
|
-
const attribute = (node, name) =>
|
|
23
|
-
(node.attributes || []).find(
|
|
24
|
-
(entry) => entry.type === "JSXAttribute" && entry.name?.type === "JSXIdentifier" && entry.name.name === name,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
const expressionOf = (attributeNode) => {
|
|
28
|
-
const value = attributeNode?.value
|
|
29
|
-
if (!value) return null
|
|
30
|
-
return value.type === "JSXExpressionContainer" ? value.expression : value
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const unwrapExpression = (node) => {
|
|
34
|
-
let current = node
|
|
35
|
-
while (["TSAsExpression", "TSTypeAssertion", "TSNonNullExpression", "ChainExpression"].includes(current?.type)) {
|
|
36
|
-
current = current.expression
|
|
37
|
-
}
|
|
38
|
-
return current
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const literalString = (node) => {
|
|
42
|
-
const value = unwrapExpression(node)
|
|
43
|
-
if (value?.type === "Literal" && typeof value.value === "string") return value.value
|
|
44
|
-
if (value?.type === "TemplateLiteral" && value.expressions.length === 0) return value.quasis[0]?.value?.cooked ?? null
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const defineContractCall = (node) => {
|
|
49
|
-
const value = unwrapExpression(node)
|
|
50
|
-
return value?.type === "CallExpression" && value.callee?.type === "Identifier" && value.callee.name === "defineContractComponent"
|
|
51
|
-
? value
|
|
52
|
-
: null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const meaningfulChildren = (openingElement) => {
|
|
56
|
-
const children = openingElement.parent?.children || []
|
|
57
|
-
return children.filter((child) => {
|
|
58
|
-
if (child.type === "JSXText") return child.value.trim().length > 0
|
|
59
|
-
if (child.type === "JSXExpressionContainer") return child.expression?.type !== "JSXEmptyExpression"
|
|
60
|
-
return true
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// -- GRAMMAR-1 -----------------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/** Product source consumes adapters; only adapter/contract owners may import Core grammar values. */
|
|
67
|
-
export const noCoreGrammarValueImportOutsideAdapter = {
|
|
68
|
-
meta: {
|
|
69
|
-
type: "problem",
|
|
70
|
-
docs: { description: "Core grammar values are imported only by branch adapters and the grammar contract owner." },
|
|
71
|
-
schema: [],
|
|
72
|
-
messages: {
|
|
73
|
-
boundary:
|
|
74
|
-
"`{{source}}` is the Core grammar value lane. Product source must consume a typed branch/composition adapter; only components/branches and components/contracts/grammar may import these values.",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
create(context) {
|
|
78
|
-
const filename = filenameOf(context)
|
|
79
|
-
if (!isProductSource(filename) || isBranchOwner(filename) || isGrammarContract(filename)) return {}
|
|
80
|
-
return {
|
|
81
|
-
ImportDeclaration(node) {
|
|
82
|
-
const source = String(node.source?.value || "")
|
|
83
|
-
if (!/^@starci\/grammar(?:\/|$)/.test(source)) return
|
|
84
|
-
const valueSpecifiers = (node.specifiers || []).filter(
|
|
85
|
-
(specifier) => node.importKind !== "type" && specifier.importKind !== "type",
|
|
86
|
-
)
|
|
87
|
-
if (valueSpecifiers.length > 0) context.report({ node, messageId: "boundary", data: { source } })
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// -- GRAMMAR-2 -----------------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
/** Surface branches are invoked through their contract/render lane, never as JSX containers. */
|
|
96
|
-
export const surfaceBranchRequiresContractRender = {
|
|
97
|
-
meta: {
|
|
98
|
-
type: "problem",
|
|
99
|
-
docs: { description: "A surface branch requires contract + render and accepts no JSX children." },
|
|
100
|
-
schema: [],
|
|
101
|
-
messages: {
|
|
102
|
-
lane: "`{{component}}` is a grammar branch. Pass both `contract` and `render`; do not use a raw surface prop lane.",
|
|
103
|
-
children:
|
|
104
|
-
"`{{component}}` received pre-built JSX children. Describe the branch with `contract` + `render` so Tree can validate and own the composition.",
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
create(context) {
|
|
108
|
-
const filename = filenameOf(context)
|
|
109
|
-
if (!isProductSource(filename) || isBranchOwner(filename)) return {}
|
|
110
|
-
return {
|
|
111
|
-
JSXOpeningElement(node) {
|
|
112
|
-
const component = jsxName(node)
|
|
113
|
-
if (!component || !SURFACES.has(component)) return
|
|
114
|
-
if (!attribute(node, "contract") || !attribute(node, "render")) {
|
|
115
|
-
context.report({ node, messageId: "lane", data: { component } })
|
|
116
|
-
}
|
|
117
|
-
for (const child of meaningfulChildren(node)) {
|
|
118
|
-
context.report({ node: child, messageId: "children", data: { component } })
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// -- GRAMMAR-3 -----------------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
const propertyName = (node) => {
|
|
128
|
-
if (node?.key?.type === "Identifier") return node.key.name
|
|
129
|
-
if (node?.key?.type === "Literal" && typeof node.key.value === "string") return node.key.value
|
|
130
|
-
return "slot"
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const forbiddenReactType = (node, seen = new Set()) => {
|
|
134
|
-
if (!node || typeof node !== "object" || seen.has(node)) return null
|
|
135
|
-
seen.add(node)
|
|
136
|
-
if (node.type === "TSTypeReference") {
|
|
137
|
-
const name = node.typeName?.type === "Identifier" ? node.typeName.name : null
|
|
138
|
-
if (name === "ReactNode" || name === "ReactElement") return name
|
|
139
|
-
}
|
|
140
|
-
if (
|
|
141
|
-
node.type === "TSQualifiedName" &&
|
|
142
|
-
node.left?.type === "Identifier" && node.left.name === "JSX" &&
|
|
143
|
-
node.right?.type === "Identifier" && node.right.name === "Element"
|
|
144
|
-
) return "JSX.Element"
|
|
145
|
-
if (
|
|
146
|
-
node.type === "TSQualifiedName" &&
|
|
147
|
-
node.left?.type === "Identifier" && node.left.name === "React" &&
|
|
148
|
-
node.right?.type === "Identifier" && ["ReactNode", "ReactElement"].includes(node.right.name)
|
|
149
|
-
) return `React.${node.right.name}`
|
|
150
|
-
for (const [key, value] of Object.entries(node)) {
|
|
151
|
-
if (key === "parent" || key === "loc" || key === "range" || key === "tokens" || key === "comments") continue
|
|
152
|
-
if (Array.isArray(value)) {
|
|
153
|
-
for (const entry of value) {
|
|
154
|
-
const found = forbiddenReactType(entry, seen)
|
|
155
|
-
if (found) return found
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
const found = forbiddenReactType(value, seen)
|
|
159
|
-
if (found) return found
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Component APIs cannot carry already-rendered React values across the grammar boundary. */
|
|
166
|
-
export const noReactnodeEscapeSlot = {
|
|
167
|
-
meta: {
|
|
168
|
-
type: "problem",
|
|
169
|
-
docs: { description: "Product component slots carry typed data/components, never ReactNode or JSX.Element." },
|
|
170
|
-
schema: [],
|
|
171
|
-
messages: {
|
|
172
|
-
escape:
|
|
173
|
-
"`{{slot}}` exposes `{{type}}`, an already-built rendering escape hatch. Pass typed data or a ComponentType through the canonical contract/render lane instead.",
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
create(context) {
|
|
177
|
-
const filename = filenameOf(context)
|
|
178
|
-
if (!isProductSource(filename) || isBranchOwner(filename) || isGrammarContract(filename)) return {}
|
|
179
|
-
return {
|
|
180
|
-
TSPropertySignature(node) {
|
|
181
|
-
const type = forbiddenReactType(node.typeAnnotation?.typeAnnotation)
|
|
182
|
-
if (type) context.report({ node, messageId: "escape", data: { slot: propertyName(node), type } })
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const collectDefinitions = () => {
|
|
189
|
-
const definitions = new Map()
|
|
190
|
-
return {
|
|
191
|
-
definitions,
|
|
192
|
-
visitor: {
|
|
193
|
-
VariableDeclarator(node) {
|
|
194
|
-
if (node.id?.type === "Identifier" && node.init) definitions.set(node.id.name, unwrapExpression(node.init))
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const resolvedDefineCall = (expression, definitions) => {
|
|
201
|
-
const direct = defineContractCall(expression)
|
|
202
|
-
if (direct) return direct
|
|
203
|
-
const value = unwrapExpression(expression)
|
|
204
|
-
return value?.type === "Identifier" ? defineContractCall(definitions.get(value.name)) : null
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// -- GRAMMAR-4 -----------------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
/** Runtime props require the ComponentType lane of defineContractComponent. */
|
|
210
|
-
export const runtimeContractContentRequiresComponentType = {
|
|
211
|
-
meta: {
|
|
212
|
-
type: "problem",
|
|
213
|
-
docs: { description: "A surface with runtime props renders a named ComponentType contract." },
|
|
214
|
-
schema: [],
|
|
215
|
-
messages: {
|
|
216
|
-
component:
|
|
217
|
-
"This runtime surface binds content instead of a ComponentType. Define a named render component and pass it as the second argument to `defineContractComponent` before sending runtime `props`.",
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
create(context) {
|
|
221
|
-
const filename = filenameOf(context)
|
|
222
|
-
if (!isProductSource(filename) || isBranchOwner(filename)) return {}
|
|
223
|
-
const { definitions, visitor } = collectDefinitions()
|
|
224
|
-
const candidates = []
|
|
225
|
-
return {
|
|
226
|
-
...visitor,
|
|
227
|
-
JSXOpeningElement(node) {
|
|
228
|
-
const component = jsxName(node)
|
|
229
|
-
if (!component || !SURFACES.has(component)) return
|
|
230
|
-
const runtimeInput = component === "SurfaceListCard"
|
|
231
|
-
? attribute(node, "props")
|
|
232
|
-
: attribute(node, "contentProps")
|
|
233
|
-
if (!runtimeInput) return
|
|
234
|
-
candidates.push({ node, render: expressionOf(attribute(node, "render")) })
|
|
235
|
-
},
|
|
236
|
-
"Program:exit"() {
|
|
237
|
-
for (const candidate of candidates) {
|
|
238
|
-
const call = resolvedDefineCall(candidate.render, definitions)
|
|
239
|
-
if (!call) continue // an imported, already-typed contract is checked by TypeScript
|
|
240
|
-
const content = unwrapExpression(call.arguments[1])
|
|
241
|
-
if (content?.type !== "Identifier") context.report({ node: candidate.node, messageId: "component" })
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// -- GRAMMAR-5 -----------------------------------------------------------------------------------
|
|
249
|
-
|
|
250
|
-
/** A rendered contract cannot claim a different identity from its Tree/surface host. */
|
|
251
|
-
export const treeContractRenderIdentity = {
|
|
252
|
-
meta: {
|
|
253
|
-
type: "problem",
|
|
254
|
-
docs: { description: "Tree/surface contract identity matches defineContractComponent identity." },
|
|
255
|
-
schema: [],
|
|
256
|
-
messages: {
|
|
257
|
-
mismatch:
|
|
258
|
-
"Host contract `{{host}}` does not match render contract `{{render}}`. One branch has one identity; use the same key in Tree/surface and defineContractComponent.",
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
create(context) {
|
|
262
|
-
const filename = filenameOf(context)
|
|
263
|
-
if (!isProductSource(filename)) return {}
|
|
264
|
-
const { definitions, visitor } = collectDefinitions()
|
|
265
|
-
const candidates = []
|
|
266
|
-
return {
|
|
267
|
-
...visitor,
|
|
268
|
-
JSXOpeningElement(node) {
|
|
269
|
-
const component = jsxName(node)
|
|
270
|
-
if (!component || !TYPED_ROOTS.has(component)) return
|
|
271
|
-
const host = literalString(expressionOf(attribute(node, "contract")))
|
|
272
|
-
const render = expressionOf(attribute(node, "render"))
|
|
273
|
-
if (host && render) candidates.push({ node, host, render })
|
|
274
|
-
},
|
|
275
|
-
"Program:exit"() {
|
|
276
|
-
for (const candidate of candidates) {
|
|
277
|
-
const call = resolvedDefineCall(candidate.render, definitions)
|
|
278
|
-
const render = literalString(call?.arguments?.[0])
|
|
279
|
-
if (render && render !== candidate.host) {
|
|
280
|
-
context.report({ node: candidate.node, messageId: "mismatch", data: { host: candidate.host, render } })
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// -- GRAMMAR-6 -----------------------------------------------------------------------------------
|
|
289
|
-
|
|
290
|
-
const exportedFunction = (node) => {
|
|
291
|
-
if (node.type === "FunctionDeclaration") return node.parent?.type === "ExportNamedDeclaration" || node.parent?.type === "ExportDefaultDeclaration"
|
|
292
|
-
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return false
|
|
293
|
-
const declarator = node.parent
|
|
294
|
-
return declarator?.type === "VariableDeclarator" && declarator.parent?.parent?.type === "ExportNamedDeclaration"
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const returnedValues = (node) => {
|
|
298
|
-
if (node.body?.type !== "BlockStatement") return [node.body]
|
|
299
|
-
return node.body.body.filter((statement) => statement.type === "ReturnStatement").map((statement) => statement.argument)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const untypedRoot = (node) => {
|
|
303
|
-
const value = unwrapExpression(node)
|
|
304
|
-
if (!value || value.type === "Literal" && value.value === null) return null
|
|
305
|
-
if (value.type === "ConditionalExpression") return untypedRoot(value.consequent) || untypedRoot(value.alternate)
|
|
306
|
-
if (value.type === "LogicalExpression") return untypedRoot(value.right)
|
|
307
|
-
if (value.type === "JSXFragment") return value
|
|
308
|
-
if (value.type !== "JSXElement") return null
|
|
309
|
-
const name = value.openingElement?.name
|
|
310
|
-
if (name?.type === "JSXIdentifier" && /^[A-Z]/.test(name.name)) return null
|
|
311
|
-
if (name?.type === "JSXMemberExpression") return null
|
|
312
|
-
return value
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** Exported block roots are typed compositions, not fragments or structural host elements. */
|
|
316
|
-
export const blockRootIsTypedComposition = {
|
|
317
|
-
meta: {
|
|
318
|
-
type: "problem",
|
|
319
|
-
docs: { description: "An exported block starts at a typed composition boundary." },
|
|
320
|
-
schema: [],
|
|
321
|
-
messages: {
|
|
322
|
-
root:
|
|
323
|
-
"A block cannot own an anonymous fragment or structural host root. Start it with Tree or a named typed composition component whose contract owns the arrangement.",
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
create(context) {
|
|
327
|
-
const filename = filenameOf(context)
|
|
328
|
-
if (!isProductSource(filename) || !filename.includes("/components/blocks/")) return {}
|
|
329
|
-
const check = (node) => {
|
|
330
|
-
if (!exportedFunction(node)) return
|
|
331
|
-
for (const value of returnedValues(node)) {
|
|
332
|
-
const root = untypedRoot(value)
|
|
333
|
-
if (root) context.report({ node: root, messageId: "root" })
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
ArrowFunctionExpression: check,
|
|
338
|
-
FunctionExpression: check,
|
|
339
|
-
FunctionDeclaration: check,
|
|
340
|
-
}
|
|
341
|
-
},
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export const rules = {
|
|
345
|
-
"no-core-grammar-value-import-outside-adapter": noCoreGrammarValueImportOutsideAdapter,
|
|
346
|
-
"surface-branch-requires-contract-render": surfaceBranchRequiresContractRender,
|
|
347
|
-
"no-reactnode-escape-slot": noReactnodeEscapeSlot,
|
|
348
|
-
"runtime-contract-content-requires-component-type": runtimeContractContentRequiresComponentType,
|
|
349
|
-
"tree-contract-render-identity": treeContractRenderIdentity,
|
|
350
|
-
"block-root-is-typed-composition": blockRootIsTypedComposition,
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
|
354
|
-
|
|
355
|
-
const severityOf = (setting) => {
|
|
356
|
-
const severity = Array.isArray(setting) ? setting[0] : setting
|
|
357
|
-
if (severity === "error") return 2
|
|
358
|
-
if (severity === "warn") return 1
|
|
359
|
-
if (severity === "off") return 0
|
|
360
|
-
return severity
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Prove every newly created production probe receives every grammar-boundary rule strictly. */
|
|
364
|
-
export const auditEffectiveConfigCoversNewFiles = (probes, expectedRules = Object.keys(rules).map((name) => `starci-fe/${name}`)) => {
|
|
365
|
-
const uncovered = []
|
|
366
|
-
for (const probe of probes || []) {
|
|
367
|
-
const config = probe?.config ?? {}
|
|
368
|
-
const missing = expectedRules.filter((name) => severityOf(config.rules?.[name]) !== 2)
|
|
369
|
-
if (missing.length > 0 || config.linterOptions?.noInlineConfig !== true) {
|
|
370
|
-
uncovered.push({ path: probe?.path ?? "<unknown>", missing, refusesInlineConfig: config.linterOptions?.noInlineConfig === true })
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return { ok: (probes || []).length > 0 && uncovered.length === 0, uncovered }
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const versionParts = (value) => {
|
|
377
|
-
const match = String(value || "").match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
378
|
-
return match ? match.slice(1).map(Number) : null
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** Canon FE V2 is compatible only with the ComponentType/Tree grammar lane introduced in 0.2.0. */
|
|
382
|
-
export const auditGrammarEslintVersionParity = ({ eslintVersion, grammarVersion } = {}) => {
|
|
383
|
-
const eslint = versionParts(eslintVersion)
|
|
384
|
-
const grammar = versionParts(grammarVersion)
|
|
385
|
-
const ok = Boolean(eslint && grammar && eslint[0] === 2 && (grammar[0] > 0 || grammar[1] >= 2))
|
|
386
|
-
return { ok, eslintVersion: eslintVersion ?? null, grammarVersion: grammarVersion ?? null, expected: "canon 2.x + grammar >=0.2.0" }
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** README headline counts are executable release metadata, not hand-kept prose. */
|
|
390
|
-
export const auditRuleReadmeCountParity = ({ readme, publishedRules = rules, publishedLaws } = {}) => {
|
|
391
|
-
const match = String(readme || "").match(/\*\*(\d+) ESLint rules, from (\d+) laws/)
|
|
392
|
-
const actualRules = Object.keys(publishedRules || {}).length
|
|
393
|
-
const actualLaws = Number(publishedLaws)
|
|
394
|
-
const declaredRules = match ? Number(match[1]) : null
|
|
395
|
-
const declaredLaws = match ? Number(match[2]) : null
|
|
396
|
-
return {
|
|
397
|
-
ok: declaredRules === actualRules && declaredLaws === actualLaws,
|
|
398
|
-
declaredRules,
|
|
399
|
-
actualRules,
|
|
400
|
-
declaredLaws,
|
|
401
|
-
actualLaws,
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** A local Python MCP/Qdrant result is usable only when it proves one clean reference worktree. */
|
|
406
|
-
export const auditReferenceReceiptPreflight = ({ reference, receipt } = {}) => {
|
|
407
|
-
const sourcePath = normalizePath(reference?.path)
|
|
408
|
-
const indexedPath = normalizePath(receipt?.sourcePath)
|
|
409
|
-
const commit = String(reference?.revision || "")
|
|
410
|
-
const revision = String(receipt?.revision || "")
|
|
411
|
-
const revisionMatches = revision.length >= 7 && commit.startsWith(revision)
|
|
412
|
-
const referenceRoot = sourcePath.includes("/.worktrees/references/")
|
|
413
|
-
const sourcePathMatches = referenceRoot && sourcePath === indexedPath
|
|
414
|
-
const clean = receipt?.dirty === false
|
|
415
|
-
const hasGeneration = typeof receipt?.generation === "string" && receipt.generation.length > 0
|
|
416
|
-
return {
|
|
417
|
-
ok: revisionMatches && sourcePathMatches && clean && hasGeneration,
|
|
418
|
-
revisionMatches,
|
|
419
|
-
referenceRoot,
|
|
420
|
-
sourcePathMatches,
|
|
421
|
-
clean,
|
|
422
|
-
hasGeneration,
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export const audits = {
|
|
427
|
-
"effective-config-covers-new-files": auditEffectiveConfigCoversNewFiles,
|
|
428
|
-
"grammar-eslint-version-parity": auditGrammarEslintVersionParity,
|
|
429
|
-
"rule-readme-count-parity": auditRuleReadmeCountParity,
|
|
430
|
-
"reference-receipt-preflight": auditReferenceReceiptPreflight,
|
|
431
|
-
}
|
|
15
|
+
/** The neutral boundary contributes no mandatory rule levels. */
|
|
16
|
+
export const recommended = {}
|
package/index.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* would leave two names for one rule and no way to tell which a message came from.
|
|
14
14
|
*/
|
|
15
15
|
import { recommended as commentsRecommended, rules as commentsRules } from "./comments.mjs"
|
|
16
|
-
import { recommended as
|
|
16
|
+
import { recommended as classNamesRecommended, rules as classNamesRules } from "./class-names.mjs"
|
|
17
17
|
import { recommended as fileLayoutRecommended, rules as fileLayoutRules } from "./file-layout.mjs"
|
|
18
18
|
import {
|
|
19
19
|
audits as grammarBoundaryAudits,
|
|
@@ -46,7 +46,11 @@ import { recommended as vendorRecommended, rules as vendorRules } from "./vendor
|
|
|
46
46
|
/** Each law's contribution, kept separate so a duplicate name is detectable rather than silent. */
|
|
47
47
|
const CONTRIBUTIONS = [
|
|
48
48
|
{ law: "comments", rules: commentsRules, recommended: commentsRecommended },
|
|
49
|
-
{
|
|
49
|
+
{
|
|
50
|
+
law: "class-names",
|
|
51
|
+
rules: classNamesRules,
|
|
52
|
+
recommended: classNamesRecommended,
|
|
53
|
+
},
|
|
50
54
|
{ law: "file-layout", rules: fileLayoutRules, recommended: fileLayoutRecommended },
|
|
51
55
|
{
|
|
52
56
|
law: "grammar-boundary",
|