@starci/eslint-canon-be 3.4.39 → 3.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 +10 -0
- package/README.md +2 -2
- package/boundary-schema.mjs +32 -4
- package/idempotency.mjs +3 -0
- package/index.mjs +2 -0
- package/infra-owner.mjs +4 -3
- package/lib/client-idempotency.mjs +403 -0
- package/lib/persistence.mjs +1 -1
- package/lib/schema-values.mjs +90 -0
- package/lib/unit-spec.mjs +6 -0
- package/operations.mjs +197 -32
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +168 -47
- package/runtime/knowledge/hfs/slots.yaml +110 -52
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +12 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +16 -6
- package/runtime/scripts/hfs/rules/database-constants.mjs +26 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +489 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +2 -2
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/package-at.mjs +62 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/suppression.mjs +1 -1
- package/unit-spec.mjs +66 -6
- package/uploads.mjs +112 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import ts from "typescript"
|
|
4
|
+
import { normalizePath } from "./path.mjs"
|
|
5
|
+
|
|
6
|
+
/** Tiers a schema file may live in: the owners of boundary data (R304). */
|
|
7
|
+
export const SCHEMA_TIERS = new Set(["domain", "platform", "integrations"])
|
|
8
|
+
|
|
9
|
+
const ZOD = /^zod(?:\/|$)/
|
|
10
|
+
/** A sibling schema module: its exports are schemas too, so a chain may start from them. */
|
|
11
|
+
const SIBLING_SCHEMA = /^\.{1,2}\/(?:.*\/)?[^/]+\.schema(?:\.[cm]?[jt]s)?$/
|
|
12
|
+
|
|
13
|
+
/** The local names the imports of a source file bind whose module specifier matches `specifier`. */
|
|
14
|
+
const importedNames = (sourceFile, specifier) => {
|
|
15
|
+
const names = new Set()
|
|
16
|
+
for (const statement of sourceFile.statements) {
|
|
17
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !specifier.test(statement.moduleSpecifier.text)) continue
|
|
18
|
+
const clause = statement.importClause
|
|
19
|
+
if (clause?.name) names.add(clause.name.text)
|
|
20
|
+
const bindings = clause?.namedBindings
|
|
21
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.add(bindings.name.text)
|
|
22
|
+
if (bindings && ts.isNamedImports(bindings)) for (const element of bindings.elements) names.add(element.name.text)
|
|
23
|
+
}
|
|
24
|
+
return names
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The identifier a call/member chain starts from (`z.string().max(5)` gives `z`), or null when it starts from anything else. */
|
|
28
|
+
const rootOf = (expression) => {
|
|
29
|
+
let current = expression
|
|
30
|
+
for (;;) {
|
|
31
|
+
if (ts.isIdentifier(current)) return current.text
|
|
32
|
+
if (ts.isCallExpression(current) || ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)
|
|
33
|
+
|| ts.isNonNullExpression(current) || ts.isParenthesizedExpression(current) || ts.isAsExpression(current)) current = current.expression
|
|
34
|
+
else return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A literal bound (`100`, `-1`, `"x"`, `true`): data a schema reads, nothing that runs. */
|
|
39
|
+
const isLiteral = (expression) => ts.isNumericLiteral(expression) || ts.isStringLiteralLike(expression)
|
|
40
|
+
|| expression.kind === ts.SyntaxKind.TrueKeyword || expression.kind === ts.SyntaxKind.FalseKeyword
|
|
41
|
+
|| (ts.isPrefixUnaryExpression(expression) && ts.isNumericLiteral(expression.operand))
|
|
42
|
+
|
|
43
|
+
/** A statement a schema file may hold: an import, a type, or `export const X = <zod chain or a sibling schema>`. */
|
|
44
|
+
const statementProblem = (statement, zodNames, declared) => {
|
|
45
|
+
if (ts.isImportDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement)) return null
|
|
46
|
+
if (ts.isExportDeclaration(statement)) return null
|
|
47
|
+
if (!ts.isVariableStatement(statement)) return `a ${ts.SyntaxKind[statement.kind]} is logic, not a schema value`
|
|
48
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
49
|
+
const init = declaration.initializer
|
|
50
|
+
if (init && isLiteral(init) && ts.isIdentifier(declaration.name)) { declared.add(declaration.name.text); continue }
|
|
51
|
+
const root = init ? rootOf(init) : null
|
|
52
|
+
if (!ts.isIdentifier(declaration.name) || root === null || !(zodNames.has(root) || declared.has(root))) {
|
|
53
|
+
return `${declaration.name.getText()} is not a zod schema (a schema file declares only zod chains and types)`
|
|
54
|
+
}
|
|
55
|
+
declared.add(declaration.name.text)
|
|
56
|
+
}
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The reasons a source text is not a pure boundary schema file: it imports zod and holds nothing but imports, types and
|
|
62
|
+
* `const X = <zod chain>` values, with no class, function, provider or other logic. An empty list means a value-only schema file.
|
|
63
|
+
*/
|
|
64
|
+
export const schemaFileProblems = (text) => {
|
|
65
|
+
const sourceFile = ts.createSourceFile("subject.schema.ts", text, ts.ScriptTarget.Latest, true)
|
|
66
|
+
const zodNames = importedNames(sourceFile, ZOD)
|
|
67
|
+
if (zodNames.size === 0) return ["it imports no zod, so it is not a boundary schema"]
|
|
68
|
+
const declared = importedNames(sourceFile, SIBLING_SCHEMA)
|
|
69
|
+
return sourceFile.statements.flatMap((statement) => {
|
|
70
|
+
const problem = statementProblem(statement, zodNames, declared)
|
|
71
|
+
return problem === null ? [] : [problem]
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CACHE = new Map()
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the sibling `<name>.schema.ts` of a `<name>.schema.spec.ts` is a real boundary schema: it exists, lives in an owner tier of R304 and is
|
|
79
|
+
* value-only (schemaFileProblems). Read from disk once per file and modification time.
|
|
80
|
+
*/
|
|
81
|
+
export const siblingIsValueSchema = (hfs, specFile) => {
|
|
82
|
+
const sibling = normalizePath(path.resolve(specFile.replace(/\.spec\.ts$/u, ".ts")))
|
|
83
|
+
if (!fs.existsSync(sibling) || !SCHEMA_TIERS.has(hfs.tierOf(sibling))) return false
|
|
84
|
+
const stamp = fs.statSync(sibling).mtimeMs
|
|
85
|
+
const cached = CACHE.get(sibling)
|
|
86
|
+
if (cached?.stamp === stamp) return cached.ok
|
|
87
|
+
const ok = schemaFileProblems(fs.readFileSync(sibling, "utf8")).length === 0
|
|
88
|
+
CACHE.set(sibling, { stamp, ok })
|
|
89
|
+
return ok
|
|
90
|
+
}
|
package/lib/unit-spec.mjs
CHANGED
|
@@ -66,6 +66,12 @@ export const unitRoleOfSpec = (hfs, filename) => {
|
|
|
66
66
|
return [...unitRoles(hfs), ...optionalRoles(hfs)].find((role) => name.endsWith(`.${role.spec}.ts`) && inRoleSlot(hfs, role, filename)) ?? null
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Whether a file is the spec of a value role (`ruleParams.be.valueRoles`: a zod schema): its subject is a declared value, so the spec parses it and builds no testing module. */
|
|
70
|
+
export const isValueRoleSpec = (hfs, filename) => {
|
|
71
|
+
const role = unitRoleOfSpec(hfs, filename)?.role
|
|
72
|
+
return role !== undefined && (hfs.ruleParams.valueRoles ?? []).includes(role)
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
/**
|
|
70
76
|
* The unit-tested role whose subject a file is (`<name>.<role>.ts`, in the role's slot, outside the test tree), or null.
|
|
71
77
|
*
|
package/operations.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The rules that hold BE-OPERATIONS-1 (R95 `BE_OPERATION_CONTRACT`).
|
|
3
3
|
*
|
|
4
|
-
* A versioned operation is declared once, in the app's typed operation table (`platform/operations`: `query
|
|
5
|
-
*
|
|
6
|
-
* from
|
|
4
|
+
* A versioned operation is declared once, in the app's typed operation table (`platform/operations`: `query({ input, output })`
|
|
5
|
+
* and `mutation({ input, output })` registered by `defineOperations`), and the contract emit derives `contracts/<app>/openapi.json`
|
|
6
|
+
* from its bounded zod schemas. An operation whose input or output the checker cannot express (`any`, `unknown`,
|
|
7
7
|
* `Record<string, unknown>`, an unbound generic) says nothing to a client, and a route that takes or answers those types by another
|
|
8
8
|
* road than the table's own `OperationRequest` and `OperationReply` is a second, untyped contract.
|
|
9
9
|
*
|
|
@@ -38,6 +38,129 @@ const isCanonType = (hfs, checker, symbol, name) => declarationsOfSymbol(checker
|
|
|
38
38
|
|
|
39
39
|
const F = ts.TypeFlags
|
|
40
40
|
|
|
41
|
+
/** Optional object fields can be absent; an operation's root input/value must itself be a JSON value. */
|
|
42
|
+
const rootMayBeAbsent = (type) => Boolean(type.flags & (F.Undefined | F.Void)) || (type.isUnion() && type.types.some(rootMayBeAbsent))
|
|
43
|
+
|
|
44
|
+
/** Follow constant aliases and declaration wrappers without trusting an asserted type. */
|
|
45
|
+
const expressionOf = (checker, node, seen = new Set()) => {
|
|
46
|
+
if (!node || seen.has(node)) return node
|
|
47
|
+
seen.add(node)
|
|
48
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) return expressionOf(checker, node.expression, seen)
|
|
49
|
+
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) || ts.isQualifiedName(node)) {
|
|
50
|
+
for (const declaration of declarationsOfSymbol(checker, checker.getSymbolAtLocation(node))) {
|
|
51
|
+
if (ts.isShorthandPropertyAssignment(declaration)) {
|
|
52
|
+
const value = checker.getShorthandAssignmentValueSymbol(declaration)
|
|
53
|
+
const home = declarationsOfSymbol(checker, value).find((candidate) => ts.isVariableDeclaration(candidate) && candidate.initializer)
|
|
54
|
+
if (home) return expressionOf(checker, home.initializer, seen)
|
|
55
|
+
}
|
|
56
|
+
if ((ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration)) && declaration.initializer) return expressionOf(checker, declaration.initializer, seen)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return node
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A schema is a named declaration of its boundary owner, including re-exports and local aliases. */
|
|
63
|
+
const ownerSchema = (hfs, checker, node, seen = new Set()) => {
|
|
64
|
+
if (!node || seen.has(node)) return false
|
|
65
|
+
seen.add(node)
|
|
66
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) return ownerSchema(hfs, checker, node.expression, seen)
|
|
67
|
+
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return false
|
|
68
|
+
const type = checker.getTypeAtLocation(node)
|
|
69
|
+
const parser = type.getProperty("safeParse")
|
|
70
|
+
if (!(parser?.getDeclarations() ?? []).some((declaration) => /\/node_modules\/zod\//.test(declaration.getSourceFile().fileName.replace(/\\/g, "/")))) return false
|
|
71
|
+
for (const declaration of declarationsOfSymbol(checker, checker.getSymbolAtLocation(node))) {
|
|
72
|
+
const source = declaration.getSourceFile()
|
|
73
|
+
const file = source.fileName
|
|
74
|
+
const module = checker.getSymbolAtLocation(source)
|
|
75
|
+
const exported = module && checker.getExportsOfModule(module).some((symbol) => declarationsOfSymbol(checker, symbol).includes(declaration))
|
|
76
|
+
if (/\.schema\.ts$/.test(file) && ["domain", "platform", "integrations"].includes(hfs.tierOf(file)) && exported) return true
|
|
77
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer && ownerSchema(hfs, checker, declaration.initializer, seen)) return true
|
|
78
|
+
if (ts.isShorthandPropertyAssignment(declaration)) {
|
|
79
|
+
for (const home of declarationsOfSymbol(checker, checker.getShorthandAssignmentValueSymbol(declaration))) {
|
|
80
|
+
if (ts.isVariableDeclaration(home) && home.initializer && ownerSchema(hfs, checker, home.initializer, seen)) return true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Read a property initializer from a closed declaration object, following an alias of that object. */
|
|
88
|
+
const propertyValue = (checker, expression, name) => {
|
|
89
|
+
const object = expressionOf(checker, expression)
|
|
90
|
+
if (!object) return null
|
|
91
|
+
const property = checker.getTypeAtLocation(object).getProperty(name)
|
|
92
|
+
for (const declaration of property?.getDeclarations() ?? []) {
|
|
93
|
+
if (ts.isPropertyAssignment(declaration)) return declaration.initializer
|
|
94
|
+
if (ts.isShorthandPropertyAssignment(declaration)) return declaration.name
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A spec registers tables to test the platform capability; such a table is local to the spec and serves no operation. */
|
|
100
|
+
const isSpecFile = (filename) => /\.(?:spec|e2e-spec|integration-spec|contract-spec)\.ts$/.test(filename.replaceAll("\\", "/"))
|
|
101
|
+
|
|
102
|
+
const TS_WRAPPERS = new Set(["TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression", "TSTypeAssertion"])
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether the table a `defineOperations` call makes leaves its file: `export const X = ...`, `export { X }` or `export default X`.
|
|
106
|
+
* A spec may keep a stub table to itself; one it exports can be imported by production code, so it is judged like any table.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} node - The CallExpression (ESTree).
|
|
109
|
+
* @returns {boolean} True when the registered table is exported.
|
|
110
|
+
*/
|
|
111
|
+
const tableLeavesFile = (node) => {
|
|
112
|
+
let value = node
|
|
113
|
+
while (TS_WRAPPERS.has(value.parent?.type)) value = value.parent
|
|
114
|
+
const parent = value.parent
|
|
115
|
+
if (parent?.type === "ExportDefaultDeclaration") return true
|
|
116
|
+
if (parent?.type !== "VariableDeclarator" || parent.init !== value || parent.id.type !== "Identifier") return false
|
|
117
|
+
if (parent.parent?.parent?.type === "ExportNamedDeclaration") return true
|
|
118
|
+
let program = parent
|
|
119
|
+
while (program.parent) program = program.parent
|
|
120
|
+
return program.body.some((statement) => {
|
|
121
|
+
if (statement.type === "ExportDefaultDeclaration") return statement.declaration?.type === "Identifier" && statement.declaration.name === parent.id.name
|
|
122
|
+
return statement.type === "ExportNamedDeclaration" && statement.specifiers.some((specifier) => specifier.local.name === parent.id.name)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The operation table must supply the runtime schemas that a generic request type cannot validate. */
|
|
127
|
+
export const operationTableSchemas = {
|
|
128
|
+
meta: {
|
|
129
|
+
type: "problem",
|
|
130
|
+
docs: { description: "Every defineOperations entry declares bounded input and output schemas from an owner's *.schema.ts file." },
|
|
131
|
+
schema: [],
|
|
132
|
+
messages: {
|
|
133
|
+
schemas: "Operation {{operation}} has no boundary schemas. Declare query({ input: InputSchema, output: OutputSchema }) or mutation(...), using named bounded zod schema exports from an owner's `*.schema.ts` file (R304/R305).",
|
|
134
|
+
table: "The operation table must be a nonempty closed declaration whose entries supply runtime input and output schemas.",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
create(context) {
|
|
138
|
+
const inSpec = isSpecFile(context.filename)
|
|
139
|
+
const hfs = hfsOf(context)
|
|
140
|
+
const { checker, toTs } = typed(context)
|
|
141
|
+
return {
|
|
142
|
+
CallExpression(node) {
|
|
143
|
+
const call = toTs(node)
|
|
144
|
+
if (!isCanonType(hfs, checker, checker.getSymbolAtLocation(call.expression), "defineOperations")) return
|
|
145
|
+
if (inSpec && !tableLeavesFile(node)) return
|
|
146
|
+
const argument = expressionOf(checker, call.arguments[0])
|
|
147
|
+
const entries = argument ? checker.getPropertiesOfType(checker.getTypeAtLocation(argument)) : []
|
|
148
|
+
if (entries.length === 0) {
|
|
149
|
+
context.report({ node, messageId: "table" })
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const declaration = entry.getDeclarations()?.find((candidate) => ts.isPropertyAssignment(candidate) || ts.isShorthandPropertyAssignment(candidate))
|
|
154
|
+
const value = declaration && expressionOf(checker, ts.isShorthandPropertyAssignment(declaration) ? declaration.name : declaration.initializer)
|
|
155
|
+
const helper = value && ts.isCallExpression(value) && ["query", "mutation"].some((name) => isCanonType(hfs, checker, checker.getSymbolAtLocation(value.expression), name))
|
|
156
|
+
const options = helper ? value.arguments[0] : null
|
|
157
|
+
if (!["input", "output"].every((name) => ownerSchema(hfs, checker, propertyValue(checker, options, name)))) context.report({ node, messageId: "schemas", data: { operation: entry.getName() } })
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
41
164
|
/**
|
|
42
165
|
* Why a type says nothing on the wire, or null when the checker can express it.
|
|
43
166
|
*
|
|
@@ -46,7 +169,7 @@ const F = ts.TypeFlags
|
|
|
46
169
|
* @param {Set<object>} seen - The types already judged (recursive types).
|
|
47
170
|
* @returns {string | null} The reason, or null.
|
|
48
171
|
*/
|
|
49
|
-
const whyUndecidable = (checker, type, seen = new Set()) => {
|
|
172
|
+
const whyUndecidable = (checker, type, seen = new Set(), schemaBacked = false) => {
|
|
50
173
|
if (seen.has(type)) return null
|
|
51
174
|
seen.add(type)
|
|
52
175
|
const flags = type.flags
|
|
@@ -59,7 +182,7 @@ const whyUndecidable = (checker, type, seen = new Set()) => {
|
|
|
59
182
|
if (type.isUnion() || type.isIntersection()) {
|
|
60
183
|
for (const member of type.types) {
|
|
61
184
|
if (member.flags & (F.Undefined | F.Void)) continue
|
|
62
|
-
const why = whyUndecidable(checker, member, seen)
|
|
185
|
+
const why = whyUndecidable(checker, member, seen, schemaBacked)
|
|
63
186
|
if (why) return why
|
|
64
187
|
}
|
|
65
188
|
return null
|
|
@@ -68,7 +191,7 @@ const whyUndecidable = (checker, type, seen = new Set()) => {
|
|
|
68
191
|
if (type.getCallSignatures().length || type.getConstructSignatures().length) return "a function"
|
|
69
192
|
if (checker.isArrayType(type) || checker.isTupleType(type)) {
|
|
70
193
|
for (const item of checker.getTypeArguments(type)) {
|
|
71
|
-
const why = whyUndecidable(checker, item, seen)
|
|
194
|
+
const why = whyUndecidable(checker, item, seen, schemaBacked)
|
|
72
195
|
if (why) return why
|
|
73
196
|
}
|
|
74
197
|
return null
|
|
@@ -77,13 +200,15 @@ const whyUndecidable = (checker, type, seen = new Set()) => {
|
|
|
77
200
|
if (own && !own.getName().startsWith("__") && (own.getDeclarations() ?? []).some((declaration) => /[\\/]typescript[\\/]lib[\\/]lib\./.test(declaration.getSourceFile().fileName))) return `${own.getName()}, which is not a JSON value`
|
|
78
201
|
const props = checker.getPropertiesOfType(type)
|
|
79
202
|
const index = checker.getIndexInfosOfType(type)
|
|
80
|
-
if (props.length === 0 && index.length === 0) return "an object type with no members"
|
|
203
|
+
if (props.length === 0 && index.length === 0 && !schemaBacked) return "an object type with no members"
|
|
81
204
|
for (const info of index) {
|
|
82
|
-
|
|
205
|
+
// Zod's strict empty object infers Record<string, never>: no key is permitted, rather than an unknown value.
|
|
206
|
+
if (schemaBacked && (info.type.flags & F.Never)) continue
|
|
207
|
+
const why = whyUndecidable(checker, info.type, seen, schemaBacked)
|
|
83
208
|
if (why) return `a record of ${why}`
|
|
84
209
|
}
|
|
85
210
|
for (const prop of props) {
|
|
86
|
-
const why = whyUndecidable(checker, checker.getTypeOfSymbol(prop), seen)
|
|
211
|
+
const why = whyUndecidable(checker, checker.getTypeOfSymbol(prop), seen, schemaBacked)
|
|
87
212
|
if (why) return `${prop.getName()}: ${why}`
|
|
88
213
|
}
|
|
89
214
|
}
|
|
@@ -104,15 +229,18 @@ export const operationContractDecidable = {
|
|
|
104
229
|
},
|
|
105
230
|
},
|
|
106
231
|
create(context) {
|
|
232
|
+
const inSpec = isSpecFile(context.filename)
|
|
107
233
|
const hfs = hfsOf(context)
|
|
108
234
|
const { checker, toTs } = typed(context)
|
|
109
|
-
|
|
110
|
-
const judge = (node, contract) => {
|
|
235
|
+
const inOwner = declaredByOperations(hfs, context.filename.replace(/\\/g, "/"))
|
|
236
|
+
const judge = (node, contract, schemaCall = null) => {
|
|
111
237
|
const symbol = contract.aliasSymbol ?? contract.getSymbol()
|
|
112
238
|
if (!isCanonType(hfs, checker, symbol, "OperationContract")) return
|
|
239
|
+
const call = schemaCall ?? (node.type === "CallExpression" ? toTs(node) : null)
|
|
240
|
+
const schemaBacked = call && ["query", "mutation"].some((name) => isCanonType(hfs, checker, checker.getSymbolAtLocation(call.expression), name)) && ["input", "output"].every((name) => ownerSchema(hfs, checker, propertyValue(checker, call.arguments[0], name)))
|
|
113
241
|
const [input, output, refusal] = (contract.objectFlags & ts.ObjectFlags.Reference) !== 0 ? checker.getTypeArguments(contract) : (contract.aliasTypeArguments ?? [])
|
|
114
242
|
for (const [label, type] of [["input", input], ["output", output]]) {
|
|
115
|
-
const why = type ? whyUndecidable(checker, type) : null
|
|
243
|
+
const why = type ? rootMayBeAbsent(type) ? "undefined or void at the root" : whyUndecidable(checker, type, new Set(), schemaBacked) : null
|
|
116
244
|
if (why) context.report({ node, messageId: "type", data: { label, why } })
|
|
117
245
|
}
|
|
118
246
|
if (refusal && !(refusal.flags & F.Never)) {
|
|
@@ -122,12 +250,34 @@ export const operationContractDecidable = {
|
|
|
122
250
|
}
|
|
123
251
|
return {
|
|
124
252
|
TSTypeReference(node) {
|
|
253
|
+
// The capability declares generic OperationContract/OperationTable bounds here; concrete registrations below
|
|
254
|
+
// still owe the same closed parsed types as operations declared anywhere else.
|
|
255
|
+
if (inOwner || inSpec) return
|
|
125
256
|
const tsNode = toTs(node)
|
|
126
257
|
const type = checker.getTypeFromTypeNode(tsNode)
|
|
127
258
|
if (type.getSymbol() || type.aliasSymbol) judge(node, type)
|
|
128
259
|
},
|
|
129
260
|
CallExpression(node) {
|
|
130
|
-
const
|
|
261
|
+
const call = toTs(node)
|
|
262
|
+
if ((inOwner || inSpec) && isCanonType(hfs, checker, checker.getSymbolAtLocation(call.expression), "defineOperations")) {
|
|
263
|
+
if (inSpec && !tableLeavesFile(node)) return
|
|
264
|
+
const argument = call.arguments[0]
|
|
265
|
+
const source = expressionOf(checker, argument)
|
|
266
|
+
const entries = argument ? checker.getPropertiesOfType(checker.getTypeAtLocation(argument)) : []
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
const contract = checker.getTypeOfSymbol(entry)
|
|
269
|
+
const actual = source ? propertyValue(checker, source, entry.getName()) : null
|
|
270
|
+
const value = expressionOf(checker, actual)
|
|
271
|
+
// Plain helper calls are judged once by their own CallExpression. A declaration annotation can widen
|
|
272
|
+
// that result before registration, so its served type also needs a check.
|
|
273
|
+
const helper = value && ts.isCallExpression(value) && ["query", "mutation"].some((name) => isCanonType(hfs, checker, checker.getSymbolAtLocation(value.expression), name)) ? value : null
|
|
274
|
+
if (inSpec || !helper || contract !== checker.getTypeAtLocation(helper)) judge(node, contract, helper)
|
|
275
|
+
}
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if (inSpec) return
|
|
279
|
+
if (inOwner &&!["query", "mutation"].some((name) => isCanonType(hfs, checker, checker.getSymbolAtLocation(call.expression), name))) return
|
|
280
|
+
const type = checker.getTypeAtLocation(call)
|
|
131
281
|
if (type.getSymbol()) judge(node, type)
|
|
132
282
|
},
|
|
133
283
|
}
|
|
@@ -136,6 +286,19 @@ export const operationContractDecidable = {
|
|
|
136
286
|
|
|
137
287
|
// -- operation-route-driven-by-table -----------------------------------------------------------------------------
|
|
138
288
|
|
|
289
|
+
/** The runtime table named by a `typeof OPERATIONS` type argument, including named aliases of that type. */
|
|
290
|
+
const tableExpressionOf = (checker, type, seen = new Set()) => {
|
|
291
|
+
if (!type || seen.has(type)) return null
|
|
292
|
+
seen.add(type)
|
|
293
|
+
if (ts.isTypeQueryNode(type)) return expressionOf(checker, type.exprName)
|
|
294
|
+
if (ts.isParenthesizedTypeNode(type)) return tableExpressionOf(checker, type.type, seen)
|
|
295
|
+
if (!ts.isTypeReferenceNode(type)) return null
|
|
296
|
+
for (const declaration of declarationsOfSymbol(checker, checker.getSymbolAtLocation(type.typeName))) {
|
|
297
|
+
if (ts.isTypeAliasDeclaration(declaration)) return tableExpressionOf(checker, declaration.type, seen)
|
|
298
|
+
}
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
139
302
|
/** The canon alias a type annotation node names (`OperationRequest`, `OperationReply`), with its table type argument. */
|
|
140
303
|
const canonReference = (hfs, checker, toTs, annotation) => {
|
|
141
304
|
let node = annotation
|
|
@@ -146,7 +309,7 @@ const canonReference = (hfs, checker, toTs, annotation) => {
|
|
|
146
309
|
for (const name of ["OperationRequest", "OperationReply"]) {
|
|
147
310
|
if (isCanonType(hfs, checker, symbol, name)) {
|
|
148
311
|
const argument = tsNode.typeArguments?.[0]
|
|
149
|
-
return { name, table: argument ? checker.getTypeFromTypeNode(argument) : null }
|
|
312
|
+
return { name, table: argument ? checker.getTypeFromTypeNode(argument) : null, expression: tableExpressionOf(checker, argument) }
|
|
150
313
|
}
|
|
151
314
|
}
|
|
152
315
|
return null
|
|
@@ -186,7 +349,8 @@ export const operationRouteEvidence = (context) => {
|
|
|
186
349
|
ts.forEachChild(node, visit)
|
|
187
350
|
}
|
|
188
351
|
for (const source of program.getSourceFiles()) {
|
|
189
|
-
|
|
352
|
+
// A table made in a spec is never an app's declared table, even when the spec exports it.
|
|
353
|
+
if (!source.isDeclarationFile && !isSpecFile(source.fileName) && hfs.slotOf(source.fileName)) visit(source)
|
|
190
354
|
}
|
|
191
355
|
return tables
|
|
192
356
|
}
|
|
@@ -328,28 +492,16 @@ export const restRouteDeclared = {
|
|
|
328
492
|
},
|
|
329
493
|
}
|
|
330
494
|
|
|
331
|
-
/** True when a pipe argument is `new OperationValidationService(...)`, the class declared by the platform capability `http-security` (resolved by symbol). */
|
|
332
|
-
const isOperationPipe = (hfs, checker, toTs, argument) => {
|
|
333
|
-
if (argument?.type !== "NewExpression") return false
|
|
334
|
-
const target = toTs(argument.callee)
|
|
335
|
-
if (!target) return false
|
|
336
|
-
return declarationsOfSymbol(checker, checker.getSymbolAtLocation(target)).some((declaration) => {
|
|
337
|
-
const file = String(declaration.getSourceFile().fileName).replace(/\\/g, "/")
|
|
338
|
-
return declaration.name?.text === "OperationValidationService" && hfs.slotOf(file) === "be.platform" && hfs.ownerOf(file)?.split("/").pop() === "http-security"
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
495
|
/** An operation route takes the table's `OperationRequest` and answers its `OperationReply`, of one table, and validates the body it takes. */
|
|
343
496
|
export const operationRouteDrivenByTable = {
|
|
344
497
|
meta: {
|
|
345
498
|
type: "problem",
|
|
346
|
-
docs: { description: "
|
|
499
|
+
docs: { description: "An operation route takes OperationRequest<Table>, validates it with OperationValidationPolicy(Table), and answers Promise<OperationReply<Table>>." },
|
|
347
500
|
schema: [],
|
|
348
501
|
messages: {
|
|
349
502
|
half: "This route takes or answers `{{one}}` but not `{{other}}`. An operation route is driven by the table on both sides: `@Body() request: OperationRequest<Table>` and `Promise<OperationReply<Table>>`.",
|
|
350
503
|
table: "The request and the reply of this route name different operation tables. One route serves one table.",
|
|
351
|
-
|
|
352
|
-
"The body of this operation route is typed by an alias, which leaves no class at run time, so the global pipe validates nothing. Bind it to the request class with `@Body(new OperationValidationService(RequestClass))` of `platform/http-security`.",
|
|
504
|
+
validation: "A generic OperationRequest is not checked by ValidationPipe. Use `@Body(new OperationValidationPolicy(OPERATIONS))` from platform/operations with the same table as OperationRequest<Table>.",
|
|
353
505
|
},
|
|
354
506
|
},
|
|
355
507
|
create(context) {
|
|
@@ -373,9 +525,20 @@ export const operationRouteDrivenByTable = {
|
|
|
373
525
|
context.report({ node: node.key, messageId: "table" })
|
|
374
526
|
return
|
|
375
527
|
}
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
528
|
+
const validated = params.some((param) => {
|
|
529
|
+
const reference = canonReference(hfs, checker, toTs, param.typeAnnotation ?? param.parameter?.typeAnnotation)
|
|
530
|
+
if (reference?.name !== "OperationRequest") return false
|
|
531
|
+
return (param.decorators ?? []).some((decorator) => {
|
|
532
|
+
if (!isImportedFrom(context, decoratorCallee(decorator), "@nestjs/common", "Body")) return false
|
|
533
|
+
const args = decoratorArguments(decorator)
|
|
534
|
+
if (args.length !== 1) return false
|
|
535
|
+
const pipe = expressionOf(checker, toTs(args[0]))
|
|
536
|
+
if (!pipe || !ts.isNewExpression(pipe) || !isCanonType(hfs, checker, checker.getSymbolAtLocation(pipe.expression), "OperationValidationPolicy")) return false
|
|
537
|
+
const table = expressionOf(checker, pipe.arguments?.[0])
|
|
538
|
+
return pipe.arguments?.length === 1 && table && request.expression && table === request.expression
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
if (!validated) context.report({ node: node.key, messageId: "validation" })
|
|
379
542
|
},
|
|
380
543
|
}
|
|
381
544
|
},
|
|
@@ -384,6 +547,7 @@ export const operationRouteDrivenByTable = {
|
|
|
384
547
|
/** Every rule of this law. */
|
|
385
548
|
export const rules = {
|
|
386
549
|
"operation-contract-decidable": operationContractDecidable,
|
|
550
|
+
"operation-table-schemas": operationTableSchemas,
|
|
387
551
|
"operation-route-driven-by-table": operationRouteDrivenByTable,
|
|
388
552
|
"rest-route-declared": restRouteDeclared,
|
|
389
553
|
}
|
|
@@ -391,6 +555,7 @@ export const rules = {
|
|
|
391
555
|
/** Every rule of this law at `error`. */
|
|
392
556
|
export const recommended = {
|
|
393
557
|
"starci-be/operation-contract-decidable": "error",
|
|
558
|
+
"starci-be/operation-table-schemas": "error",
|
|
394
559
|
"starci-be/operation-route-driven-by-table": "error",
|
|
395
560
|
"starci-be/rest-route-declared": "error",
|
|
396
561
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
schema: starci/canon-pins@1
|
|
2
2
|
purpose: >-
|
|
3
|
-
The exact version of every dependency
|
|
3
|
+
The exact version of every shared dependency in the StarCi family, including package-owned runtime dependencies. One version per dependency for the whole
|
|
4
4
|
family. Raising a version is a change to this file first (with the package bump when it is a @starci package),
|
|
5
5
|
then the repositories' own upgrade lane (each sets the exact version and reinstalls; nothing else installs a @starci package). Read on 2026-09-29 from the packages under
|
|
6
6
|
.claude/packages and from the installed node_modules of the six product repositories; where they disagreed the
|
|
@@ -10,7 +10,8 @@ purpose: >-
|
|
|
10
10
|
two, pinned here with a why (2026-10-01: every such conflict but graphql was already pinned). The runtime's example apps
|
|
11
11
|
merged the same way on 2026-10-01: no dependency outside this file was declared at two versions across their sides, so
|
|
12
12
|
every choice is a pin (each side's older @starci pins and graphql moved to the version here). `side` names the side whose code uses
|
|
13
|
-
the dependency; the app root package.json
|
|
13
|
+
the dependency; the app root package.json declares the direct dependencies and tools its sides need. Package-owned
|
|
14
|
+
runtime dependencies are declared by their package, not by the app.
|
|
14
15
|
pins:
|
|
15
16
|
# --- @starci packages. Every @starci package is a PUBLISHED npm package (install: registry): a product repository
|
|
16
17
|
# installs the exact pinned version from the npm registry in the root and every workspace package.json, never `file:`.
|
|
@@ -22,28 +23,28 @@ pins:
|
|
|
22
23
|
source: packages/heroicons/package.json
|
|
23
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.'
|
|
24
25
|
'@starci/grammar':
|
|
25
|
-
version: 0.
|
|
26
|
+
version: 0.12.0
|
|
26
27
|
group: starci
|
|
27
28
|
install: registry
|
|
28
29
|
side: fe
|
|
29
30
|
source: packages/grammar/package.json
|
|
30
31
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
31
32
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.4.
|
|
33
|
+
version: 3.4.41
|
|
33
34
|
group: starci
|
|
34
35
|
install: registry
|
|
35
36
|
side: be
|
|
36
37
|
source: packages/eslint/be/package.json
|
|
37
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.'
|
|
38
39
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
40
|
+
version: 8.4.40
|
|
40
41
|
group: starci
|
|
41
42
|
install: registry
|
|
42
43
|
side: fe
|
|
43
44
|
source: packages/eslint/fe/package.json
|
|
44
45
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe 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; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
45
46
|
'@starci/stylelint-canon':
|
|
46
|
-
version: 2.0.
|
|
47
|
+
version: 2.0.11
|
|
47
48
|
group: starci
|
|
48
49
|
install: registry
|
|
49
50
|
side: fe
|
|
@@ -62,7 +63,7 @@ pins:
|
|
|
62
63
|
side: both
|
|
63
64
|
source: packages/prettier-config/package.json
|
|
64
65
|
'@starci/jest-preset':
|
|
65
|
-
version: 2.3.
|
|
66
|
+
version: 2.3.6
|
|
66
67
|
group: starci
|
|
67
68
|
install: registry
|
|
68
69
|
side: be
|
|
@@ -76,14 +77,14 @@ pins:
|
|
|
76
77
|
source: packages/test-world/package.json
|
|
77
78
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
79
|
'@starci/cli':
|
|
79
|
-
version: 1.7.
|
|
80
|
+
version: 1.7.33
|
|
80
81
|
group: starci
|
|
81
82
|
install: registry
|
|
82
83
|
side: both
|
|
83
84
|
source: packages/cli/package.json
|
|
84
85
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
86
|
'@starci/hfs':
|
|
86
|
-
version: 4.7.
|
|
87
|
+
version: 4.7.33
|
|
87
88
|
group: starci
|
|
88
89
|
install: registry
|
|
89
90
|
side: both
|
|
@@ -238,6 +239,11 @@ pins:
|
|
|
238
239
|
group: framework
|
|
239
240
|
side: be
|
|
240
241
|
why: 'the Postgres driver behind TypeORM; the reference app declares ^8.12.0 and resolves 8.23.1.'
|
|
242
|
+
bullmq:
|
|
243
|
+
version: 6.3.11
|
|
244
|
+
group: framework
|
|
245
|
+
side: be
|
|
246
|
+
why: 'the queue transport of both editions behind platform/queue (ruling:R212/case-10); starci app add queue|job declares it in a lite app together with its Redis, and both example apps resolve this version.'
|
|
241
247
|
reflect-metadata:
|
|
242
248
|
version: 0.2.2
|
|
243
249
|
group: framework
|
|
@@ -305,6 +311,11 @@ pins:
|
|
|
305
311
|
group: framework
|
|
306
312
|
side: fe
|
|
307
313
|
why: 'the GraphQL client of every front end (fe-stack graphql): auth and no-auth clients with the link chain in modules/api/graphql; version read from the starci-academy package-lock on 2026-10-13.'
|
|
314
|
+
graphql-ws:
|
|
315
|
+
version: 6.2.1
|
|
316
|
+
group: framework
|
|
317
|
+
side: both
|
|
318
|
+
why: 'the subscription transport: the front end opens it only in the Apollo clients folder (fe-stack graphql, GraphQLWsLink of @apollo/client/link/subscriptions) and the back end serves it only from platform/realtime; the version resolved by the full-app lockfile on 2026-10-24.'
|
|
308
319
|
'@graphql-codegen/cli':
|
|
309
320
|
version: 7.4.6
|
|
310
321
|
group: tooling
|
|
@@ -370,6 +381,16 @@ pins:
|
|
|
370
381
|
group: ui
|
|
371
382
|
side: fe
|
|
372
383
|
why: 'the base of the one application icon set (fe-stack icons, with @starci/heroicons for the glyphs it lacks); eslint-canon-fe (icon.mjs) admits @heroicons/react/24/outline and /16/solid.'
|
|
384
|
+
'@dicebear/core':
|
|
385
|
+
version: 10.4.0
|
|
386
|
+
group: ui
|
|
387
|
+
side: fe
|
|
388
|
+
why: 'runtime dependency of @starci/grammar for its seeded circle avatars; apps use the Avatar seed prop and never declare or import @dicebear/*; pinned with @dicebear/styles.'
|
|
389
|
+
'@dicebear/styles':
|
|
390
|
+
version: 10.4.0
|
|
391
|
+
group: ui
|
|
392
|
+
side: fe
|
|
393
|
+
why: 'runtime dependency of @starci/grammar supplying its fixed avatar DNA style; apps use the Avatar seed prop and never declare or import @dicebear/*; pinned with @dicebear/core.'
|
|
373
394
|
'next-themes':
|
|
374
395
|
version: 0.4.6
|
|
375
396
|
group: ui
|
|
@@ -481,6 +502,16 @@ pins:
|
|
|
481
502
|
group: framework
|
|
482
503
|
side: be
|
|
483
504
|
why: 'the Apollo driver of @nestjs/graphql; the full-app resolution on 2026-10-13. Stays on 13.x with @nestjs/graphql (accepted advisory, major deferred to alpha.19).'
|
|
505
|
+
'@aws-sdk/client-s3':
|
|
506
|
+
version: 3.1150.0
|
|
507
|
+
group: framework
|
|
508
|
+
side: be
|
|
509
|
+
why: 'the S3 transport of integrations/object-storage, the one ObjectStorage adapter of both editions (MinIO, S3, Spaces, Supabase Storage); 3.1150.0 is the release of 2026-10-12, chosen over newer ones that had not aged two weeks.'
|
|
510
|
+
'@aws-sdk/s3-request-presigner':
|
|
511
|
+
version: 3.1150.0
|
|
512
|
+
group: framework
|
|
513
|
+
side: be
|
|
514
|
+
why: 'signs the presigned PUT and GET of integrations/object-storage locally, the same release as client-s3.'
|
|
484
515
|
'@apollo/server':
|
|
485
516
|
version: 5.5.1
|
|
486
517
|
group: framework
|
|
@@ -496,6 +527,31 @@ pins:
|
|
|
496
527
|
group: framework
|
|
497
528
|
side: be
|
|
498
529
|
why: 'the socket.io platform of @nestjs/websockets; the @nestjs/core pin line.'
|
|
530
|
+
socket.io:
|
|
531
|
+
version: 4.8.3
|
|
532
|
+
group: framework
|
|
533
|
+
side: be
|
|
534
|
+
why: 'the socket.io server the IoAdapter of platform/realtime builds (types and the adapter contract); the exact version @nestjs/platform-socket.io 11.2.5 depends on, so the workspace keeps one copy (4.8.4 is the latest on 2026-10-24).'
|
|
535
|
+
'@socket.io/postgres-adapter':
|
|
536
|
+
version: 0.5.0
|
|
537
|
+
group: framework
|
|
538
|
+
side: be
|
|
539
|
+
why: 'the stateless socket.io adapter of a back end without Redis that runs several api replicas (LISTEN/NOTIFY, no sticky sessions, websocket only), imported only by platform/realtime (knowledge/patterns/be/realtime.yaml BE-REALTIME-7); the latest registry version on 2026-10-24.'
|
|
540
|
+
'@socket.io/redis-adapter':
|
|
541
|
+
version: 8.3.0
|
|
542
|
+
group: framework
|
|
543
|
+
side: be
|
|
544
|
+
why: 'the sharded Redis adapter (createShardedAdapter, Redis 7 or later) of a back end with Redis declared that runs several api replicas, imported only by platform/realtime; the latest registry version on 2026-10-24.'
|
|
545
|
+
ioredis:
|
|
546
|
+
version: 5.11.1
|
|
547
|
+
group: framework
|
|
548
|
+
side: be
|
|
549
|
+
why: 'the one Redis client of a back end: the cache, the optional peer bullmq loads for the queue of platform/queue, and the Redis hub and sharded socket.io adapter of platform/realtime (its docs recommend ioredis over redis); the latest 5.x on 2026-10-24, because typeorm 0.3.31 declares ioredis ^5.0.4 as a peer and ioredis 6.0.0 (the latest) does not satisfy it.'
|
|
550
|
+
'@types/pg':
|
|
551
|
+
version: 8.23.1
|
|
552
|
+
group: framework
|
|
553
|
+
side: be
|
|
554
|
+
why: 'the types of the pg driver the Postgres hub of platform/realtime opens a LISTEN connection with; the pg pin line, the latest registry version on 2026-10-24.'
|
|
499
555
|
'@as-integrations/express5':
|
|
500
556
|
version: 1.1.2
|
|
501
557
|
group: framework
|