@starci/eslint-canon-fe 8.4.40 → 8.4.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/README.md +4 -0
- package/app-shell.mjs +161 -0
- package/docs/app-shell.md +50 -0
- package/index.mjs +2 -0
- package/lib/app-shell-cluster.mjs +66 -0
- package/lib/app-shell-create.mjs +33 -0
- package/lib/app-shell-owner-source.mjs +124 -0
- package/lib/app-shell-owner.mjs +65 -0
- package/lib/app-shell-scope.mjs +79 -0
- package/lib/app-shell-visible.mjs +92 -0
- package/lib/app-shell.mjs +148 -0
- package/lib/why.mjs +25 -0
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +2 -2
- package/runtime/knowledge/hfs/slots.yaml +8 -6
- package/runtime/modules/goal/source-phrases.yaml +95 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/declaration-slots.mjs +6 -4
- package/runtime/scripts/hfs/rule-params-shape.mjs +5 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +2 -2
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Traverse only the visible bar composition and its local React owners. */
|
|
2
|
+
import { functionOf, variableOf } from "./bindings.mjs"
|
|
3
|
+
import { grammarName, propOf, resolveLocal } from "./app-shell.mjs"
|
|
4
|
+
|
|
5
|
+
const BAR_SLOTS = {
|
|
6
|
+
WorkspaceShell: ["header", "compactHeader"],
|
|
7
|
+
TopBar: ["brand", "navigation", "actions"],
|
|
8
|
+
NavigationFeatureNav: ["identity", "navigation", "actions", "compactNavigationTrigger"],
|
|
9
|
+
DropdownMenu: ["trigger"],
|
|
10
|
+
Popover: ["trigger"],
|
|
11
|
+
Drawer: ["trigger"],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Visible JSX composition only: menu, popover and drawer bodies are outside the navbar band. */
|
|
15
|
+
export const visitVisible = (context, expression, visit, seen = new Set(), branch = () => "both", ancestors = []) => {
|
|
16
|
+
const node = resolveLocal(context, expression)
|
|
17
|
+
if (!node || seen.has(node)) return
|
|
18
|
+
const visited = new Set(seen).add(node)
|
|
19
|
+
const next = (child) => visitVisible(context, child, visit, visited, branch, ancestors)
|
|
20
|
+
switch (node.type) {
|
|
21
|
+
case "JSXExpressionContainer": next(node.expression); break
|
|
22
|
+
case "JSXFragment": for (const child of node.children) next(child); break
|
|
23
|
+
case "ConditionalExpression": visitConditional(node, next, branch); break
|
|
24
|
+
case "LogicalExpression": visitLogical(context, node, next, branch); break
|
|
25
|
+
case "ArrayExpression": for (const item of node.elements) next(item); break
|
|
26
|
+
case "JSXElement": visitElement(context, node, visit, visited, branch, ancestors); break
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const visitConditional = (node, next, branch) => {
|
|
31
|
+
const choice = branch(node)
|
|
32
|
+
if (choice !== "alternate") next(node.consequent)
|
|
33
|
+
if (choice !== "consequent") next(node.alternate)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const visitLogical = (context, node, next, branch) => {
|
|
37
|
+
const left = resolveLocal(context, node.left)
|
|
38
|
+
const known = left?.type === "Literal"
|
|
39
|
+
let right = node.operator === "&&" ? Boolean(left?.value) : !left?.value
|
|
40
|
+
if (node.operator === "??") right = left?.value === null
|
|
41
|
+
if (!known) {
|
|
42
|
+
const choice = branch(node)
|
|
43
|
+
if (choice === "both") { next(node.left); next(node.right); return }
|
|
44
|
+
right = choice === "consequent"
|
|
45
|
+
}
|
|
46
|
+
next(right ? node.right : node.left)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const visitElement = (context, node, visit, seen, branch, ancestors) => {
|
|
50
|
+
const opening = node.openingElement
|
|
51
|
+
const name = grammarName(context, opening)
|
|
52
|
+
if (visit(opening, name, ancestors) === false) return
|
|
53
|
+
const inside = [...ancestors, opening]
|
|
54
|
+
const next = (child) => visitVisible(context, child, visit, seen, branch, inside)
|
|
55
|
+
if (name === null && opening.name.type === "JSXIdentifier") {
|
|
56
|
+
const fn = functionOf(context, resolveLocal(context, opening.name)) ?? variableOf(context, opening.name)?.defs.find((definition) => definition.type === "FunctionName")?.node
|
|
57
|
+
if (fn) visitFunction(context, fn, visit, seen, branch, inside)
|
|
58
|
+
}
|
|
59
|
+
const slots = BAR_SLOTS[name]
|
|
60
|
+
if (slots) for (const slot of slots) next(propOf(context, opening, slot).value)
|
|
61
|
+
if (["DropdownMenu", "Popover", "Drawer"].includes(name)) return
|
|
62
|
+
for (const child of node.children) next(child)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Visit a rendered owner's returns with the same branch, overlay and local-binding semantics as inline JSX. */
|
|
66
|
+
export const visitFunction = (context, fn, visit, seen, branch, ancestors) => {
|
|
67
|
+
if (fn.body.type !== "BlockStatement") return visitVisible(context, fn.body, visit, seen, branch, ancestors)
|
|
68
|
+
visitStatements(context, fn.body.body, visit, seen, branch, ancestors)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const visitStatements = (context, statements, visit, seen, branch, ancestors) => {
|
|
72
|
+
for (const statement of statements) {
|
|
73
|
+
if (statement.type === "ReturnStatement") {
|
|
74
|
+
visitVisible(context, statement.argument, visit, seen, branch, ancestors)
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
if (statement.type === "BlockStatement" && visitStatements(context, statement.body, visit, seen, branch, ancestors)) return true
|
|
78
|
+
if (statement.type === "IfStatement") {
|
|
79
|
+
if (visitIf(context, statement, visit, seen, branch, ancestors)) return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const visitIf = (context, statement, visit, seen, branch, ancestors) => {
|
|
86
|
+
const condition = resolveLocal(context, statement.test)
|
|
87
|
+
let choice = branch(statement)
|
|
88
|
+
if (condition?.type === "Literal") choice = condition.value ? "consequent" : "alternate"
|
|
89
|
+
const consequent = choice !== "alternate" && visitStatements(context, [statement.consequent], visit, seen, branch, ancestors)
|
|
90
|
+
const alternate = choice !== "consequent" && statement.alternate && visitStatements(context, [statement.alternate], visit, seen, branch, ancestors)
|
|
91
|
+
return choice === "consequent" && consequent || choice === "alternate" && alternate || consequent && alternate
|
|
92
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Static readers for the visible Grammar app-bar composition, including local JSX owners. */
|
|
2
|
+
import { attributeValue, stringOf } from "./ast.mjs"
|
|
3
|
+
import { functionOf, importOf, unwrap, variableOf } from "./bindings.mjs"
|
|
4
|
+
|
|
5
|
+
const GRAMMAR_SOURCE = /^@starci\/grammar(?:\/|$)/
|
|
6
|
+
/** Grammar controls whose visible hit target belongs to the navbar band's size contract. */
|
|
7
|
+
const BAR_CONTROLS = new Set(["Button", "IconButton", "Switch", "AvatarButton", "PlainPressable", "IdentityLockup", "SearchField", "Input", "TextField", "Select", "TextAction"])
|
|
8
|
+
const BAR_WRAPPERS = new Set(["GrammarRoot", "WorkspaceShell", "TopBar", "NavigationFeatureNav", "Region", "PageContainer", "NavLandmark", "Form", "Fieldset", "Field", "ButtonGroup", "DropdownMenu", "Popover", "Drawer", "Tooltip"])
|
|
9
|
+
const ACTION_PROPS = ["onPress", "onClick", "onAction", "onValueChange", "onSelectionChange", "onSelectedChange", "onChange", "onInputChange"]
|
|
10
|
+
const INTERACTION_PROPS = new Set(["label", "value", "defaultValue", "selectedKey", "selectedKeys", "selectionMode", "href", ...ACTION_PROPS])
|
|
11
|
+
|
|
12
|
+
/** Bar policy also counts labelled/value/selection/action field owners, so an extra control cannot vanish from the cluster. */
|
|
13
|
+
export const isBarControl = (context, opening, name) => {
|
|
14
|
+
if (BAR_CONTROLS.has(name)) return true
|
|
15
|
+
if (!name || !/^[A-Z]/.test(name) || BAR_WRAPPERS.has(name)) return false
|
|
16
|
+
if (opening.attributes.some((entry) => entry.type === "JSXAttribute" && INTERACTION_PROPS.has(entry.name.name))) return true
|
|
17
|
+
return [...INTERACTION_PROPS].some((prop) => propOf(context, opening, prop).value !== null)
|
|
18
|
+
}
|
|
19
|
+
/** A literal or constant's initializer, without type-only wrappers. */
|
|
20
|
+
export const resolveLocal = (context, expression, seen = new Set()) => {
|
|
21
|
+
const node = unwrap(expression)
|
|
22
|
+
if (node?.type === "MemberExpression") return resolveMember(context, node, seen)
|
|
23
|
+
if (node?.type === "CallExpression") return resolveCall(context, node, seen)
|
|
24
|
+
if (!["Identifier", "JSXIdentifier"].includes(node?.type) || seen.has(node)) return node
|
|
25
|
+
const definition = variableOf(context, node)?.defs.find((entry) => entry.type === "Variable")
|
|
26
|
+
if (!definition || definition.parent?.kind !== "const") return node
|
|
27
|
+
seen.add(node)
|
|
28
|
+
return resolveLocal(context, initializerOf(definition.node, node), seen)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolveCall = (context, node, seen) => {
|
|
32
|
+
if (seen.has(node)) return node
|
|
33
|
+
seen.add(node)
|
|
34
|
+
const fn = functionOf(context, resolveLocal(context, node.callee, seen))
|
|
35
|
+
if (!fn) return node
|
|
36
|
+
if (fn.body.type !== "BlockStatement") return resolveLocal(context, fn.body, seen)
|
|
37
|
+
if (fn.body.body.some((statement) => !["VariableDeclaration", "ReturnStatement"].includes(statement.type))) return node
|
|
38
|
+
const returned = fn.body.body.find((statement) => statement.type === "ReturnStatement")
|
|
39
|
+
return returned ? resolveLocal(context, returned.argument, seen) : node
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resolveMember = (context, node, seen) => {
|
|
43
|
+
const key = node.computed ? stringOf(node.property) : node.property.name
|
|
44
|
+
if (!key) return node
|
|
45
|
+
const property = objectValue(context, node.object, key, seen)
|
|
46
|
+
return property.known && property.value !== null ? resolveLocal(context, property.value, seen) : node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const initializerOf = (declaration, node) => {
|
|
50
|
+
if (declaration.id.type === "ObjectPattern") {
|
|
51
|
+
const property = declaration.id.properties.find((entry) => entry.type === "Property" && entry.value.name === node.name)
|
|
52
|
+
if (!property) return node
|
|
53
|
+
return { type: "MemberExpression", object: declaration.init, property: property.key, computed: property.computed }
|
|
54
|
+
}
|
|
55
|
+
return declaration.init
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The imported Grammar component's name; aliases and namespace imports keep their identity. */
|
|
59
|
+
export const grammarName = (context, opening) => {
|
|
60
|
+
const name = opening.name
|
|
61
|
+
if (name.type === "JSXIdentifier") {
|
|
62
|
+
const canonical = expressionName(context, name)
|
|
63
|
+
if (canonical) return canonical
|
|
64
|
+
return variableOf(context, name)?.defs.length ? null : name.name
|
|
65
|
+
}
|
|
66
|
+
if (name.type !== "JSXMemberExpression" || name.object.type !== "JSXIdentifier") return null
|
|
67
|
+
return expressionName(context, { type: "MemberExpression", object: name.object, property: name.property, computed: false })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const expressionName = (context, expression) => {
|
|
71
|
+
const resolved = resolveLocal(context, expression)
|
|
72
|
+
if (resolved?.type === "MemberExpression") {
|
|
73
|
+
return namespaceName(context, resolved.object, resolved.computed ? stringOf(resolved.property) : resolved.property.name)
|
|
74
|
+
}
|
|
75
|
+
if (!["Identifier", "JSXIdentifier"].includes(resolved?.type)) return null
|
|
76
|
+
const imported = importOf(context, resolved)
|
|
77
|
+
return imported && GRAMMAR_SOURCE.test(imported.source) ? imported.imported : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const namespaceName = (context, expression, property) => {
|
|
81
|
+
const object = resolveLocal(context, expression)
|
|
82
|
+
if (!["Identifier", "JSXIdentifier"].includes(object?.type)) return null
|
|
83
|
+
const imported = importOf(context, object)
|
|
84
|
+
return imported && GRAMMAR_SOURCE.test(imported.source) ? property : null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const objectValue = (context, expression, name, seen = new Set()) => {
|
|
88
|
+
const node = resolveLocal(context, expression)
|
|
89
|
+
if (node?.type === "ConditionalExpression") return conditionalObjectValue(context, node, name, seen)
|
|
90
|
+
if (node?.type !== "ObjectExpression" || seen.has(node)) return { known: false, value: null }
|
|
91
|
+
seen.add(node)
|
|
92
|
+
let result = { known: true, value: null }
|
|
93
|
+
for (const property of node.properties) {
|
|
94
|
+
if (property.type === "SpreadElement") {
|
|
95
|
+
const spread = objectValue(context, property.argument, name, seen)
|
|
96
|
+
if (!spread.known || spread.value !== null) result = spread
|
|
97
|
+
} else if (!property.computed && (property.key.name ?? property.key.value) === name) {
|
|
98
|
+
result = { known: true, value: property.value }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const conditionalObjectValue = (context, node, name, seen) => {
|
|
105
|
+
if (seen.has(node)) return { known: false, value: null }
|
|
106
|
+
const visited = new Set(seen).add(node)
|
|
107
|
+
const values = [node.consequent, node.alternate].map((branch) => objectValue(context, branch, name, new Set(visited)))
|
|
108
|
+
return { known: values.every((value) => value.known && value.value === null), value: null }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The effective prop, respecting JSX spread order; unknown later spreads cannot prove a value. */
|
|
112
|
+
export const propOf = (context, opening, name) => {
|
|
113
|
+
let result = { known: true, value: null }
|
|
114
|
+
for (const entry of opening.attributes) {
|
|
115
|
+
if (entry.type === "JSXSpreadAttribute") {
|
|
116
|
+
const spread = objectValue(context, entry.argument, name)
|
|
117
|
+
if (!spread.known || spread.value !== null) result = spread
|
|
118
|
+
} else if (entry.name.name === name) result = { known: true, value: attributeValue(entry), present: true }
|
|
119
|
+
}
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A statically proven string prop, or null for missing/unknown values. */
|
|
124
|
+
export const textProp = (context, opening, name) => {
|
|
125
|
+
const prop = propOf(context, opening, name)
|
|
126
|
+
return prop.known ? stringOf(resolveLocal(context, prop.value)) : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The source spelling is semantic evidence for an icon or action whose translated label is dynamic. */
|
|
130
|
+
export const semanticText = (context, opening) => {
|
|
131
|
+
const values = ["source", "icon", "label", "aria-label", "id", ...ACTION_PROPS].map((name) => {
|
|
132
|
+
const prop = propOf(context, opening, name)
|
|
133
|
+
return prop.value ? context.sourceCode.getText(prop.value) : ""
|
|
134
|
+
})
|
|
135
|
+
return values.join(" ")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The right-cluster role and the Grammar kind that must draw it. */
|
|
139
|
+
export const clusterRole = (context, opening, name) => {
|
|
140
|
+
if (name === "Switch") return "theme"
|
|
141
|
+
if (name === "AvatarButton") return "account"
|
|
142
|
+
const text = semanticText(context, opening)
|
|
143
|
+
if (/language|locale|translate/i.test(text)) return "language"
|
|
144
|
+
if (/notification|bell/i.test(text)) return "notifications"
|
|
145
|
+
if (/account|avatar|profile/i.test(text)) return "account"
|
|
146
|
+
if (/theme|darkmode|color.?mode/i.test(text)) return "theme"
|
|
147
|
+
return null
|
|
148
|
+
}
|
package/lib/why.mjs
CHANGED
|
@@ -29,6 +29,31 @@ export const why = {
|
|
|
29
29
|
en: "`<file>` adds horizontal wrapper padding to a Grammar list or its section caption, stacking the content inset.",
|
|
30
30
|
fix: "Remove the wrapper padding and use ListBox inset=\"panel\" so headers, captions and rows share the panel content edge.",
|
|
31
31
|
},
|
|
32
|
+
"app-shell-frame": {
|
|
33
|
+
code: "FE_APP_SHELL_FRAME",
|
|
34
|
+
en: "The app frame must use Grammar WorkspaceShell with its fixed viewport.",
|
|
35
|
+
fix: "Compose WorkspaceShell viewport=\"fixed\" and supply its header, navigation and primary slots.",
|
|
36
|
+
},
|
|
37
|
+
"no-shell-bottom-tabs": {
|
|
38
|
+
code: "FE_APP_SHELL_BOTTOM_TABS",
|
|
39
|
+
en: "Bottom tab navigation replaces the required mobile sidebar drawer.",
|
|
40
|
+
fix: "Use WorkspaceShell navigationDrawer and Sidebar presentation=\"drawer\".",
|
|
41
|
+
},
|
|
42
|
+
"navbar-control-size": {
|
|
43
|
+
code: "FE_NAVBAR_CONTROL_SIZE",
|
|
44
|
+
en: "A navbar control does not use Grammar's shared control height.",
|
|
45
|
+
fix: "Set size=\"control\" on every visible bar control.",
|
|
46
|
+
},
|
|
47
|
+
"navbar-cluster": {
|
|
48
|
+
code: "FE_NAVBAR_CLUSTER",
|
|
49
|
+
en: "Navbar action component kinds or order differ from the app-shell canon.",
|
|
50
|
+
fix: "Compose Language IconButton, theme Switch, notification IconButton and account AvatarButton in that order.",
|
|
51
|
+
},
|
|
52
|
+
"no-navbar-create": {
|
|
53
|
+
code: "FE_NAVBAR_CREATE",
|
|
54
|
+
en: "A create action is composed into the global navbar.",
|
|
55
|
+
fix: "Move the create action to the page that owns it.",
|
|
56
|
+
},
|
|
32
57
|
"no-env-outside-config": {
|
|
33
58
|
code: "FE_ENV_OWNER",
|
|
34
59
|
en: "`<file>` reads an environment variable. Only `modules/config` may read env.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "8.4.
|
|
3
|
+
"version": "8.4.41",
|
|
4
4
|
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@ pins:
|
|
|
23
23
|
source: packages/heroicons/package.json
|
|
24
24
|
why: 'StarCi custom Heroicons-compatible cuts; eslint-canon-fe (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
|
|
25
25
|
'@starci/grammar':
|
|
26
|
-
version: 0.12.
|
|
26
|
+
version: 0.12.1
|
|
27
27
|
group: starci
|
|
28
28
|
install: registry
|
|
29
29
|
side: fe
|
|
@@ -37,7 +37,7 @@ pins:
|
|
|
37
37
|
source: packages/eslint/be/package.json
|
|
38
38
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
39
39
|
'@starci/eslint-canon-fe':
|
|
40
|
-
version: 8.4.
|
|
40
|
+
version: 8.4.41
|
|
41
41
|
group: starci
|
|
42
42
|
install: registry
|
|
43
43
|
side: fe
|
|
@@ -251,8 +251,8 @@ ruleParams:
|
|
|
251
251
|
socket.io: [platform/realtime]
|
|
252
252
|
graphql-subscriptions: [platform/realtime]
|
|
253
253
|
graphql-ws: [platform/realtime]
|
|
254
|
-
"@socket.io/postgres-adapter": [platform/realtime] # the stateless socket.io adapter of a scaled lite back end
|
|
255
|
-
"@socket.io/redis-adapter": [platform/realtime] # the sharded socket.io adapter of a scaled
|
|
254
|
+
"@socket.io/postgres-adapter": [platform/realtime] # the stateless socket.io adapter of a scaled lite back end without Redis
|
|
255
|
+
"@socket.io/redis-adapter": [platform/realtime] # the sharded socket.io adapter of a scaled back end on the declared Redis
|
|
256
256
|
pg: [platform/realtime] # the LISTEN connection of the Postgres hub; every other query goes through TypeORM
|
|
257
257
|
# R87 BE_CQRS_SHAPE no-event-bus: the in-process event vocabulary the rule refuses. `imports` maps a module
|
|
258
258
|
# specifier to the member names that are an in-process event; `[]` flags the package itself (its emitter packages
|
|
@@ -371,11 +371,11 @@ ruleParams:
|
|
|
371
371
|
# ruling:R212/case-10 (R212 HFS_BACKING_UNDECLARED / HFS_BACKING_UNUSED, R276 STACKS_LITE_SELFHOSTED_DB): THE backing services a back
|
|
372
372
|
# end may declare in hfs.json sides.be.backing. `clients` are the libraries that consume the service: one of them as a dependency
|
|
373
373
|
# is its consumer, and under lite none of them may be a dependency while the service is undeclared. `patterns` are the declared
|
|
374
|
-
# patterns that need the service. `images` are the image repositories (the last path segment) a
|
|
375
|
-
#
|
|
374
|
+
# patterns that need the service. `images` are the image repositories (the last path segment) a stack runs it from.
|
|
375
|
+
# `editions` limits admission: owner:lite-vector-is-pgvector keeps vector search on Supabase pgvector in lite.
|
|
376
376
|
backing:
|
|
377
377
|
redis: {clients: [bullmq, "@nestjs/bullmq", bull, "@nestjs/bull", ioredis, redis, "@keyv/redis", cache-manager-redis-yet, cache-manager-ioredis-yet, "@socket.io/redis-adapter"], patterns: [queue, fenced-job], images: [redis, redis-stack, redis-stack-server]}
|
|
378
|
-
qdrant: {clients: ["@qdrant/js-client-rest", "@qdrant/qdrant-js"], patterns: [], images: [qdrant]}
|
|
378
|
+
qdrant: {clients: ["@qdrant/js-client-rest", "@qdrant/qdrant-js"], patterns: [], images: [qdrant], editions: [full]}
|
|
379
379
|
# Edition lite (ruleParams(manifest, 'be', 'lite') merges these keys over the base; a map merges one level deep): the schema
|
|
380
380
|
# authority is the Supabase migration tree (supabase/migrations/*.sql, forward-only), so a lite back end holds no TypeORM
|
|
381
381
|
# migration or entity under be/ (R34/R35 read schemaAuthority; be.persistence's lite overlay allows neither). The unit roles,
|
|
@@ -389,6 +389,8 @@ ruleParams:
|
|
|
389
389
|
schemaAuthority: supabase
|
|
390
390
|
infraOwners:
|
|
391
391
|
kafkajs: []
|
|
392
|
+
"@qdrant/js-client-rest": []
|
|
393
|
+
"@qdrant/qdrant-js": []
|
|
392
394
|
|
|
393
395
|
slots:
|
|
394
396
|
|
|
@@ -764,7 +766,7 @@ slots:
|
|
|
764
766
|
- id: app.starcistacks
|
|
765
767
|
since: 3.1.0
|
|
766
768
|
profiles: [app]
|
|
767
|
-
editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing;
|
|
769
|
+
editionWhy: "owner:lite-production-swarm: lite requires declared docker-swarm production with Supabase backing; owner:dev-docker-prod-swarm keeps dependencies in Docker Linux containers in dev and the app and dependencies on Swarm in prod except hosted Supabase."
|
|
768
770
|
exemplifiedBy:
|
|
769
771
|
- {path: packages/hfs/templates/app/skeleton/.starcistacks/, edition: full}
|
|
770
772
|
- {path: packages/hfs/templates/app/skeleton-lite/.starcistacks/, edition: lite}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# modules/goal/source-phrases.yaml
|
|
3
|
+
#
|
|
4
|
+
# The Vietnamese phrase lists runtime source matches against owner or product
|
|
5
|
+
# text (scripts/lib/source-phrases.mjs). This is DATA for matchers, never
|
|
6
|
+
# owner-visible messages - those live in modules/i18n/messages/ (i18n.mjs).
|
|
7
|
+
# Each list holds the Vietnamese alternatives one matcher adds to its English
|
|
8
|
+
# pattern (a regex fragment when the source pattern used regex syntax, a plain
|
|
9
|
+
# word otherwise); the consumer is named above each group. The leaf keys are
|
|
10
|
+
# declared Vietnamese fields (scripts/lib/language.mjs DECLARED_VIETNAMESE_FIELDS),
|
|
11
|
+
# like the signal phrase lists of archetypes.yaml.
|
|
12
|
+
# ============================================================================
|
|
13
|
+
|
|
14
|
+
schema: starci/module-goal-source-phrases@1
|
|
15
|
+
|
|
16
|
+
phrases:
|
|
17
|
+
|
|
18
|
+
# packages/eslint/fe/lib/app-shell-create.mjs - localized labels of create actions prohibited in the navbar.
|
|
19
|
+
appShell:
|
|
20
|
+
product: ['Tạo mới', 'tao moi']
|
|
21
|
+
|
|
22
|
+
# scripts/agent/bias.mjs extractRoutingBias - the routing markers an owner
|
|
23
|
+
# prompt types before a pool alias. Regex alternatives; syllables join with \s*.
|
|
24
|
+
bias:
|
|
25
|
+
prefer: ['ưu\s*tiên', 'uu\s*tien', 'u-u\s*tien', prefer]
|
|
26
|
+
avoid: ['không\s*dùng', 'khong\s*dung', 'tránh', 'tranh', avoid, 'don''t\s+use']
|
|
27
|
+
only: ['chỉ\s*dùng', 'chi\s*dung', 'only\s+use', only]
|
|
28
|
+
|
|
29
|
+
# scripts/checks/check-guidance-commands.mjs excusedBy - a negation or a
|
|
30
|
+
# refusal verb, in either language, excuses a refused command in guidance.
|
|
31
|
+
guidance:
|
|
32
|
+
negation: ['không', 'đừng', 'cấm', 'tránh']
|
|
33
|
+
refused: ['bị chặn', 'bị từ chối']
|
|
34
|
+
|
|
35
|
+
# scripts/kernel/ask-server.mjs drawReplyDecision - a Telegram reply made only
|
|
36
|
+
# of accept words (plus optionally "golden") accepts a drawing. accept is a
|
|
37
|
+
# word list; golden holds regex alternatives.
|
|
38
|
+
drawReply:
|
|
39
|
+
accept: ['duyệt', 'đồng', 'ý', 'chấp', 'nhận', 'được', 'đẹp', 'rồi', 'nhé', 'nha', 'ừ']
|
|
40
|
+
golden: ['(?:đặt làm |làm )?(?:mẫu|ảnh|hình) chuẩn', 'làm mẫu']
|
|
41
|
+
|
|
42
|
+
# scripts/kernel/progress-rca.mjs causesOf - the blocker/report phrases of
|
|
43
|
+
# each failure cause, whichever language the report was written in.
|
|
44
|
+
rca:
|
|
45
|
+
missingPaths: ['không tồn tại', 'vắng mặt', '0 tệp', 'quét 0']
|
|
46
|
+
grantTooNarrow: ['ngoài owned', 'ngoài allowlist', 'ngoài grant', 'ngoài own']
|
|
47
|
+
toolTimeout: ['30 giây', 'ngắt sau']
|
|
48
|
+
testGap: ['kiểm thử hồi quy']
|
|
49
|
+
checkerUnavailable: ['không khả dụng']
|
|
50
|
+
|
|
51
|
+
# scripts/supervisor/owed.mjs - notes addressed to the supervisor, conditions
|
|
52
|
+
# only the owner can meet, and the label heuristics.
|
|
53
|
+
owed:
|
|
54
|
+
supervisorVerbs: ['cho', 'cần', 'chờ', 'đợi', 'phản hồi(?: của)?', 'hỏi']
|
|
55
|
+
ownerWord: ['chủ sở hữu']
|
|
56
|
+
orWord: ['hoặc']
|
|
57
|
+
beyondAuthority: ['ngoài thẩm quyền']
|
|
58
|
+
ownerOnly: ['bàn giao', 'mật khẩu', 'thanh toán']
|
|
59
|
+
workerDied: ['không nộp report']
|
|
60
|
+
contractConflict: ['mâu thuẫn']
|
|
61
|
+
decision: ['ủy quyền', 'uỷ quyền', '\bquyết\b']
|
|
62
|
+
|
|
63
|
+
# scripts/work/draw-feedback.mjs - the owner-note classification words
|
|
64
|
+
# (GOLDEN_WORDS, product-direction, grammar, knowledge).
|
|
65
|
+
drawNote:
|
|
66
|
+
golden: ['mẫu chuẩn', 'ảnh chuẩn', 'hình chuẩn']
|
|
67
|
+
product: ['luôn', 'không bao giờ', 'mọi', 'tất cả', 'từ nay', 'màu', 'thương hiệu', 'phông', 'chữ', 'khoảng cách', 'bo góc', 'đổ bóng', 'biểu tượng']
|
|
68
|
+
grammar: ['thành phần mới', 'biến thể', 'thiếu thành phần', 'không có thành phần']
|
|
69
|
+
knowledge: ['kiến thức', 'quy tắc chung', 'nguyên tắc chung']
|
|
70
|
+
|
|
71
|
+
# scripts/work/draw/draw-quality.mjs - internal-copy jargon, actor and
|
|
72
|
+
# success words a render source's visible copy may carry in either language.
|
|
73
|
+
drawCopy:
|
|
74
|
+
sourceLabel: ['nguồn']
|
|
75
|
+
internalVocabulary: ['nguồn hiện tại', 'hệ thống lõi']
|
|
76
|
+
actor: ['người dùng', 'chủ']
|
|
77
|
+
success: ['đã cài', 'đang hoạt động', 'sẵn sàng', 'đã xác nhận', 'thành công']
|
|
78
|
+
|
|
79
|
+
# scripts/work/ui-archetype.mjs archetypeOf - surface title/intent words in
|
|
80
|
+
# either language that derive an archetype when ui.archetype is absent.
|
|
81
|
+
uiArchetype:
|
|
82
|
+
wizard: ['thiết lập', 'các bước']
|
|
83
|
+
form: ['chỉnh sửa', 'tạo mới', 'cài đặt', 'đăng nhập']
|
|
84
|
+
dashboard: ['tổng quan', 'trang chủ']
|
|
85
|
+
|
|
86
|
+
# scripts/work/grammar-proposal.mjs MD_FIELDS - the section markers of a
|
|
87
|
+
# markdown grammar proposal in either language.
|
|
88
|
+
proposal:
|
|
89
|
+
gap: ['không có', 'thiếu']
|
|
90
|
+
anatomy: ['giải phẫu']
|
|
91
|
+
|
|
92
|
+
# scripts/lib/redact.mjs - the verification-code label a secret value may sit
|
|
93
|
+
# behind (regex alternative; a separator between syllables is [ _-]?).
|
|
94
|
+
redact:
|
|
95
|
+
otpLabel: ['mã[ _-]?(?:otp|xác[ _-]?(?:thực|minh))']
|
|
@@ -311,7 +311,7 @@ const packageIndexSource = workspace => {
|
|
|
311
311
|
};
|
|
312
312
|
|
|
313
313
|
/** The source entry a workspace import resolves to without a build: its export targets mapped back to source, `src/index.ts` for the package root. */
|
|
314
|
-
function workspaceSourceEntry(ts, workspace, specifier) {
|
|
314
|
+
export function workspaceSourceEntry(ts, workspace, specifier) {
|
|
315
315
|
if (!workspace.workspace || !workspace.name || !sameOrUnder(specifier, workspace.name)) return null;
|
|
316
316
|
const isRoot = specifier === workspace.name;
|
|
317
317
|
const candidates = isRoot && !workspace.exports ? manifestEntryTargets(workspace.manifest) : exportCandidates(workspace, specifier);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (a slot with `appKind`) and by declared pattern (a slot with `pattern: <name>` is enabled by `sides.<side>.patterns: [<name>]`, one
|
|
3
3
|
// mechanism for pattern slots). A cross-cutting pattern can instead be owned by the side's canonical proof scenario catalog.
|
|
4
4
|
// slots.mjs asks this file to judge a side's lists and to say whether a slot is enabled for a repository.
|
|
5
|
+
import { judgedInEdition } from './edition-slots.mjs';
|
|
5
6
|
|
|
6
7
|
/** The problems of a side's `optionalSlots` and `patterns` against the manifest. */
|
|
7
8
|
export function optionalSlotProblems(manifest, side, s) {
|
|
@@ -24,11 +25,12 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
24
25
|
return bad;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/** The problems of a side's `backing` list: every name is
|
|
28
|
-
export function backingProblems(manifest, side, s) {
|
|
29
|
-
const services = Object.
|
|
28
|
+
/** The problems of a side's `backing` list: every name is admitted by ruleParams.be.backing in this edition. */
|
|
29
|
+
export function backingProblems(manifest, side, s, edition = 'full') {
|
|
30
|
+
const services = Object.entries(manifest.ruleParams?.be?.backing ?? {})
|
|
31
|
+
.filter(([, entry]) => judgedInEdition(entry, edition)).map(([name]) => name);
|
|
30
32
|
return (s.backing ?? []).filter((name) => !services.includes(name))
|
|
31
|
-
.map((name) => `sides.${side}.backing names ${name}, which is not
|
|
33
|
+
.map((name) => `sides.${side}.backing names ${name}, which is not admitted in edition ${edition} by ruleParams.be.backing (${services.join(', ')})`);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** The problems of the shape of a side's `patterns` list (a unique list of pattern names). */
|
|
@@ -6,13 +6,15 @@ import { paramNamesOk } from './param-names.mjs';
|
|
|
6
6
|
import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
|
|
7
7
|
import { NAME, namedMapValid, PACKAGE_NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
|
|
8
8
|
import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
|
|
9
|
+
import { ruleEditionProblems } from './edition-slots.mjs';
|
|
9
10
|
|
|
10
11
|
const PROFILES = ['be', 'fe'];
|
|
11
12
|
const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'valueRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds', 'backing'];
|
|
12
|
-
const backingEntryOk = (entry) => isPlainObject(entry) && Object.keys(entry).
|
|
13
|
+
const backingEntryOk = (entry) => isPlainObject(entry) && Object.keys(entry).every((key) => ['clients', 'images', 'patterns', 'editions'].includes(key))
|
|
13
14
|
&& nonemptyUniqueList(entry.clients, (name) => PACKAGE_NAME.test(String(name)))
|
|
14
15
|
&& Array.isArray(entry.patterns) && entry.patterns.every((name) => NAME.test(String(name))) && new Set(entry.patterns).size === entry.patterns.length
|
|
15
|
-
&& nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
|
|
16
|
+
&& nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
|
|
17
|
+
&& ruleEditionProblems(entry, 'backing service').length === 0;
|
|
16
18
|
const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
|
|
17
19
|
const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
|
|
18
20
|
const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
|
|
@@ -51,7 +53,7 @@ function beSpecificProblems(be, manifest) {
|
|
|
51
53
|
const eventBus = be.eventBus;
|
|
52
54
|
if (!isPlainObject(eventBus) || Object.keys(eventBus).sort(byCodeUnit).join() !== 'classes,imports' || !memberMap(eventBus.imports, true) || !memberMap(eventBus.classes, false)) bad.push('ruleParams.be.eventBus must be {imports: {<module>: [unique PascalCase members]}, classes: {<module>: [non-empty unique PascalCase classes]}}');
|
|
53
55
|
if (!paramNamesOk(be.paramNames)) bad.push('ruleParams.be.paramNames must be a non-empty list of unique {type | typeSuffix, names, nameSuffix?} entries');
|
|
54
|
-
if (!namedMapValid(be.backing, backingEntryOk)) bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories]}');
|
|
56
|
+
if (!namedMapValid(be.backing, backingEntryOk)) bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories], editions?: [unique known editions]}');
|
|
55
57
|
if (!roleList(be.suffixes)) bad.push('ruleParams.be.suffixes must be a non-empty list of unique kebab-case role suffixes');
|
|
56
58
|
if (!roleList(be.bannedSuffixes)) bad.push('ruleParams.be.bannedSuffixes must be a non-empty list of unique kebab-case suffixes');
|
|
57
59
|
else if (roleList(be.suffixes) && be.suffixes.some((x) => be.bannedSuffixes.includes(x))) bad.push('ruleParams.be.suffixes and bannedSuffixes must be disjoint');
|
|
@@ -19,9 +19,9 @@ function requiredAppKindProblems(manifest, side, declaration) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Problems for one side declaration, preserving the manifest's checks and their order. */
|
|
22
|
-
export function sideProblems(manifest, side, declaration) {
|
|
22
|
+
export function sideProblems(manifest, side, declaration, edition = 'full') {
|
|
23
23
|
const bad = sideAppKindProblems(manifest, side, declaration.apps);
|
|
24
|
-
bad.push(...optionalSlotProblems(manifest, side, declaration), ...backingProblems(manifest, side, declaration));
|
|
24
|
+
bad.push(...optionalSlotProblems(manifest, side, declaration), ...backingProblems(manifest, side, declaration, edition));
|
|
25
25
|
for (const read of declaration.reads ?? []) if (!manifest.sides[side].reads.includes(read)) bad.push(`sides.${side}.reads names ${read}; ${side} may read only ${manifest.sides[side].reads.join(', ') || 'nothing of the other side'}`);
|
|
26
26
|
bad.push(...requiredAppKindProblems(manifest, side, declaration));
|
|
27
27
|
return bad;
|
|
@@ -140,7 +140,7 @@ export function resolveRepoDeclaration(manifest, declaration, { file = HFS_DECLA
|
|
|
140
140
|
if (declaration.kind === RUNTIME_KIND) return runtimeDeclaration(manifest, declaration, file, side);
|
|
141
141
|
const { edition, known, valid } = declarationEdition(manifest, declaration);
|
|
142
142
|
if (!valid) fail('HFS_EDITION_INVALID', `hfs.json edition is ${JSON.stringify(declaration.edition)}; the editions this manifest knows are ${known.join(', ')} (absent means full)`, { file, edition: declaration.edition });
|
|
143
|
-
const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name]));
|
|
143
|
+
const bad = PROFILES.flatMap((name) => sideProblems(manifest, name, declaration.sides[name], edition));
|
|
144
144
|
if (bad.length) declarationInvalid(bad, file);
|
|
145
145
|
if (side !== null && !PROFILES.includes(side)) fail('HFS_DECLARATION_INVALID', `${side} is not a side of an app (be, fe)`, { file, side });
|
|
146
146
|
// A provider declared on any connection enables the provider slots of every profile (a fe side declares none): each view carries the union.
|
|
@@ -56,6 +56,45 @@ const ASSIGNED_UNQUOTED = new RegExp(
|
|
|
56
56
|
SECRET_NAME + String.raw`(?!['"\`]|env\()([A-Za-z0-9_+=-]{12,})(?![A-Za-z0-9_+=.(\[-])`,
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
+
const SQL_ROLE_HEAD = /^\s*(?:CREATE|ALTER)\s+(?:ROLE|USER)\b/i;
|
|
60
|
+
const SQL_STATEMENT = /[^;\r\n]+/g;
|
|
61
|
+
const SQL_PASSWORD_WORD = /\bPASSWORD\s+/gi;
|
|
62
|
+
const PSQL_PASSWORD_REFERENCE = /:(?:'[A-Za-z_]\w*'|"[A-Za-z_]\w*")/y;
|
|
63
|
+
const SQL_PASSWORD_QUOTED = /'([^'\r\n]{12,})'/y;
|
|
64
|
+
const ASSIGNED_PLACEHOLDER = /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i;
|
|
65
|
+
const SQL_PASSWORD_RULE = { name: 'assigned-secret' };
|
|
66
|
+
|
|
67
|
+
/** Read only password tokens in role statements; sticky value readers never retry at later character positions. */
|
|
68
|
+
function sqlPasswordParts(file, text) {
|
|
69
|
+
const references = [], literals = [];
|
|
70
|
+
if (!/\.(?:sql|sh)(?:\.tpl)?$/i.test(file ?? '')) return { references, literals };
|
|
71
|
+
for (const statement of text.matchAll(SQL_STATEMENT)) {
|
|
72
|
+
if (!SQL_ROLE_HEAD.test(statement[0])) continue;
|
|
73
|
+
for (const keyword of statement[0].matchAll(SQL_PASSWORD_WORD)) {
|
|
74
|
+
const start = statement.index + keyword.index + keyword[0].length;
|
|
75
|
+
PSQL_PASSWORD_REFERENCE.lastIndex = start;
|
|
76
|
+
SQL_PASSWORD_QUOTED.lastIndex = start;
|
|
77
|
+
const reference = PSQL_PASSWORD_REFERENCE.exec(text);
|
|
78
|
+
const literal = SQL_PASSWORD_QUOTED.exec(text);
|
|
79
|
+
if (reference) references.push([start, start + reference[0].length]);
|
|
80
|
+
if (literal) literals.push(literal[1]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { references, literals };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Psql variable references retain their positions while the assignment heuristic sees no literal value there. */
|
|
87
|
+
function maskedReferences(text, references) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let cursor = 0;
|
|
90
|
+
for (const [start, end] of references) {
|
|
91
|
+
parts.push(text.slice(cursor, start), ' '.repeat(end - start));
|
|
92
|
+
cursor = end;
|
|
93
|
+
}
|
|
94
|
+
parts.push(text.slice(cursor));
|
|
95
|
+
return parts.join('');
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
const NAME_WORD = (word, tail) => new RegExp(`^${NAME_REST}${SEGMENT_BEFORE}(?:${word})${tail}`);
|
|
60
99
|
const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$)`);
|
|
61
100
|
const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
|
|
@@ -84,27 +123,37 @@ export const SECRET_PATTERNS = [
|
|
|
84
123
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
85
124
|
// form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
|
|
86
125
|
// A SCREAMING_SNAKE quoted value in source is a public code (an enum member whose value is an upper-case code), not a credential.
|
|
87
|
-
{ name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder:
|
|
126
|
+
{ name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder: ASSIGNED_PLACEHOLDER,
|
|
88
127
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
|
|
89
128
|
// An unquoted value in TypeScript or JavaScript source is an identifier or a type (`token: InjectionToken`,
|
|
90
129
|
// `MODULE_OPTIONS_TOKEN: DATABASE_OPTIONS_TOKEN`), never a literal; the unquoted form is the dotenv, compose and toml shape.
|
|
91
|
-
{ name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder:
|
|
130
|
+
{ name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder: ASSIGNED_PLACEHOLDER,
|
|
92
131
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/ },
|
|
93
132
|
];
|
|
94
133
|
|
|
95
134
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
const
|
|
135
|
+
/** Matched pattern metadata stays private; callers receive pattern names, never credential values. */
|
|
136
|
+
function matchingSecretPatterns(file, text) {
|
|
137
|
+
const matches = [];
|
|
138
|
+
const sql = sqlPasswordParts(file, text);
|
|
139
|
+
const assignments = maskedReferences(text, sql.references);
|
|
99
140
|
for (const rule of SECRET_PATTERNS) {
|
|
100
141
|
if (rule.skipFile?.test(file ?? '')) continue;
|
|
101
|
-
const hit = rule.re.exec(text);
|
|
102
|
-
if (hit && rule.
|
|
103
|
-
if (hit && !rule.placeholder?.test(hit[1] ?? '')) names.push(rule.name);
|
|
142
|
+
const hit = rule.re.exec(rule.name === 'assigned-secret' ? assignments : text);
|
|
143
|
+
if (hit && !rule.placeholder?.test(hit[1] ?? '')) matches.push({ rule, value: hit[1] ?? '' });
|
|
104
144
|
}
|
|
105
|
-
|
|
145
|
+
for (const value of sql.literals) if (!ASSIGNED_PLACEHOLDER.test(value)) matches.push({ rule: SQL_PASSWORD_RULE, value });
|
|
146
|
+
return matches;
|
|
106
147
|
}
|
|
107
148
|
|
|
149
|
+
/** The strict secret-shape judgement used by Work custody, including uppercase keyword-assigned credentials. */
|
|
150
|
+
export const secretPatternHits = (file, text) => matchingSecretPatterns(file, text).map(({ rule }) => rule.name);
|
|
151
|
+
|
|
152
|
+
/** The source judgement also admits public SCREAMING_SNAKE codes; every provider shape remains refused. */
|
|
153
|
+
export const secretHits = (file, text) => matchingSecretPatterns(file, text)
|
|
154
|
+
.filter(({ rule, value }) => !(rule.sourceCode?.test(value) && /\.[cm]?[jt]sx?$/.test(file ?? '')))
|
|
155
|
+
.map(({ rule }) => rule.name);
|
|
156
|
+
|
|
108
157
|
const BADGE_BEGIN = '<!-- >>> starci app sync (managed block; do not edit) >>> -->';
|
|
109
158
|
const BADGE_END = '<!-- <<< starci app sync <<< -->';
|
|
110
159
|
|