Move inline code examples out of normative contracts
identifier-normalization, no-hardcoded-vendors, vendor-installer-verification, and build-version-display follow the go-database split: rules in contract.md, snippets in README.md. Routed contract reads get cheaper; examples stay available on demand. Lint now also rejects stale kit/patterns references. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
29
rules/patterns/build-version-display/README.md
Normal file
29
rules/patterns/build-version-display/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Build Version Display Pattern Notes
|
||||
|
||||
This file keeps examples. The normative rules live in `contract.md`.
|
||||
|
||||
## Frontend (JS/TS build tools)
|
||||
|
||||
```ts
|
||||
// vite.config.ts / webpack.config.js
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? "dev"),
|
||||
}
|
||||
|
||||
// Footer component
|
||||
<footer>v{__APP_VERSION__}</footer>
|
||||
```
|
||||
|
||||
## Go (server-rendered HTML)
|
||||
|
||||
```go
|
||||
// main.go
|
||||
var Version = "dev"
|
||||
|
||||
// Build: go build -ldflags "-X main.Version=1.4.2"
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- base template -->
|
||||
<footer>v{{ .Version }}</footer>
|
||||
```
|
||||
@@ -1,18 +1,13 @@
|
||||
# Contract: Build Version Display
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Every web application must display the current build version in the page footer so that users and support staff can identify exactly which version is running.
|
||||
|
||||
---
|
||||
Version: 1.1
|
||||
|
||||
## Rule
|
||||
|
||||
The build version **must** be visible in the footer on every page of the web application.
|
||||
The build version **must** be visible in the footer on every page of the web application,
|
||||
so users and support staff can identify exactly which version is running.
|
||||
|
||||
---
|
||||
See `README.md` for implementation snippets.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -22,42 +17,6 @@ The build version **must** be visible in the footer on every page of the web app
|
||||
- Format: any human-readable string that uniquely identifies the build — a semver tag, a git commit SHA, or a combination (e.g. `1.4.2`, `1.4.2-abc1234`, `abc1234`).
|
||||
- The version text must be legible but visually subordinate — use a muted color and small font size so it does not compete with page content.
|
||||
|
||||
---
|
||||
|
||||
## Recommended implementation
|
||||
|
||||
**Frontend (JS/TS build tools)**
|
||||
|
||||
Expose the version through an environment variable at build time and reference it in the footer component:
|
||||
|
||||
```ts
|
||||
// vite.config.ts / webpack.config.js
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? "dev"),
|
||||
}
|
||||
|
||||
// Footer component
|
||||
<footer>v{__APP_VERSION__}</footer>
|
||||
```
|
||||
|
||||
**Go (server-rendered HTML)**
|
||||
|
||||
Inject via `-ldflags` at build time and pass to the template:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
var Version = "dev"
|
||||
|
||||
// Build: go build -ldflags "-X main.Version=1.4.2"
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- base template -->
|
||||
<footer>v{{ .Version }}</footer>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is NOT allowed
|
||||
|
||||
- Omitting the version from any page, including error pages.
|
||||
|
||||
54
rules/patterns/identifier-normalization/README.md
Normal file
54
rules/patterns/identifier-normalization/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Identifier Normalization Pattern Notes
|
||||
|
||||
This file keeps examples. The normative rules live in `contract.md`.
|
||||
|
||||
## Go — сравнение
|
||||
|
||||
```go
|
||||
import "strings"
|
||||
|
||||
func SameIdentifier(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
}
|
||||
```
|
||||
|
||||
## Go — дедупликация
|
||||
|
||||
```go
|
||||
func deduplicateBySerial(items []Device) []Device {
|
||||
seen := make(map[string]struct{})
|
||||
result := items[:0]
|
||||
for _, item := range items {
|
||||
key := strings.ToLower(item.SerialNumber)
|
||||
if _, exists := seen[key]; !exists {
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## SQL — поиск и уникальность
|
||||
|
||||
Поиск:
|
||||
|
||||
```sql
|
||||
SELECT * FROM devices WHERE LOWER(serial_number) = LOWER(?);
|
||||
```
|
||||
|
||||
Уникальный индекс (MySQL / MariaDB):
|
||||
|
||||
```sql
|
||||
-- Collation ci обеспечивает case-insensitive уникальность
|
||||
ALTER TABLE devices MODIFY serial_number VARCHAR(255)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE devices ADD UNIQUE INDEX uniq_serial (serial_number);
|
||||
```
|
||||
|
||||
SQLite:
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX uniq_serial ON devices (LOWER(serial_number));
|
||||
```
|
||||
@@ -1,13 +1,13 @@
|
||||
# Contract: Identifier Normalization
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
|
||||
## Purpose
|
||||
|
||||
Правила хранения и сравнения идентификаторов оборудования:
|
||||
серийные номера, вендоры, версии прошивок, партномера, артикулы.
|
||||
|
||||
---
|
||||
See `README.md` for Go and SQL examples.
|
||||
|
||||
## Правило
|
||||
|
||||
@@ -17,11 +17,8 @@ Version: 1.0
|
||||
```
|
||||
Пришло: "SN-001-ABC" → хранится: "SN-001-ABC"
|
||||
Пришло: "sn-001-abc" → это тот же объект, не дубликат
|
||||
Пришло: "Sn-001-Abc" → то же самое
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Применяется к полям
|
||||
|
||||
- Серийный номер (`serial_number`, `serial`)
|
||||
@@ -30,61 +27,13 @@ Version: 1.0
|
||||
- Партномер (`part_number`, `part_no`)
|
||||
- Артикул (`article`, `sku`)
|
||||
|
||||
---
|
||||
|
||||
## Реализация
|
||||
|
||||
### Go — сравнение
|
||||
|
||||
```go
|
||||
import "strings"
|
||||
|
||||
func SameIdentifier(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
}
|
||||
```
|
||||
|
||||
### Go — дедупликация
|
||||
|
||||
```go
|
||||
func deduplicateBySerial(items []Device) []Device {
|
||||
seen := make(map[string]struct{})
|
||||
result := items[:0]
|
||||
for _, item := range items {
|
||||
key := strings.ToLower(item.SerialNumber)
|
||||
if _, exists := seen[key]; !exists {
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
Ключ в map — всегда `strings.ToLower(value)`. Сам объект сохраняется с оригинальным значением.
|
||||
|
||||
### SQL — поиск и уникальность
|
||||
|
||||
Поиск:
|
||||
```sql
|
||||
SELECT * FROM devices WHERE LOWER(serial_number) = LOWER(?);
|
||||
```
|
||||
|
||||
Уникальный индекс (MySQL / MariaDB):
|
||||
```sql
|
||||
-- Collation ci обеспечивает case-insensitive уникальность
|
||||
ALTER TABLE devices MODIFY serial_number VARCHAR(255)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE devices ADD UNIQUE INDEX uniq_serial (serial_number);
|
||||
```
|
||||
|
||||
SQLite:
|
||||
```sql
|
||||
CREATE UNIQUE INDEX uniq_serial ON devices (LOWER(serial_number));
|
||||
```
|
||||
|
||||
---
|
||||
- Go: сравнение только через `strings.EqualFold`, никогда `==`.
|
||||
- Go: ключ дедупликации в map — `strings.ToLower(value)`; сам объект хранит оригинал.
|
||||
- SQL-поиск: `WHERE LOWER(col) = LOWER(?)`.
|
||||
- Уникальность: MySQL/MariaDB — case-insensitive collation (`utf8mb4_unicode_ci`) +
|
||||
unique index; SQLite — `CREATE UNIQUE INDEX ... ON t (LOWER(col))`.
|
||||
|
||||
## Что не делать
|
||||
|
||||
|
||||
42
rules/patterns/no-hardcoded-vendors/README.md
Normal file
42
rules/patterns/no-hardcoded-vendors/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# No Hardcoded Vendors Pattern Notes
|
||||
|
||||
This file keeps examples. The normative rules live in `contract.md`.
|
||||
|
||||
## Запрещено
|
||||
|
||||
```go
|
||||
if device.Vendor == "Dell" { ... }
|
||||
if strings.Contains(model, "PowerEdge") { ... }
|
||||
switch vendor {
|
||||
case "HP", "HPE", "Hewlett Packard": ...
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// Запрещено — список вендоров в коде
|
||||
var knownVendors = []string{"Dell", "HP", "Cisco", "Lenovo"}
|
||||
```
|
||||
|
||||
## Правильно
|
||||
|
||||
```go
|
||||
// Смотрим на возможности объекта, не на имя вендора
|
||||
if device.HasIPMI { ... }
|
||||
if device.ParserType == "redfish" { ... }
|
||||
```
|
||||
|
||||
Маппинг в конфиге:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
vendor_parsers:
|
||||
dell: redfish
|
||||
hp: ilo
|
||||
cisco: ucs
|
||||
```
|
||||
|
||||
Маппинг в БД:
|
||||
|
||||
```sql
|
||||
SELECT parser_type FROM vendor_registry WHERE LOWER(vendor) = LOWER(?);
|
||||
```
|
||||
@@ -1,64 +1,23 @@
|
||||
# Contract: No Hardcoded Vendors or Models
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
|
||||
## Purpose
|
||||
|
||||
Запрет на хардкод названий вендоров, моделей и партномеров в коде.
|
||||
|
||||
---
|
||||
See `README.md` for code examples.
|
||||
|
||||
## Правило
|
||||
|
||||
Названия вендоров, моделей, серий оборудования и партномеров **не появляются в коде**.
|
||||
Они приходят из данных: БД, конфига, входного документа, справочника.
|
||||
|
||||
---
|
||||
|
||||
## Что запрещено
|
||||
|
||||
```go
|
||||
// Запрещено
|
||||
if device.Vendor == "Dell" { ... }
|
||||
if strings.Contains(model, "PowerEdge") { ... }
|
||||
switch vendor {
|
||||
case "HP", "HPE", "Hewlett Packard": ...
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// Запрещено — список вендоров в коде
|
||||
var knownVendors = []string{"Dell", "HP", "Cisco", "Lenovo"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что делать вместо
|
||||
|
||||
Логика определяется по полям из данных, не по названию вендора:
|
||||
|
||||
```go
|
||||
// Правильно — смотрим на возможности объекта, не на имя вендора
|
||||
if device.HasIPMI { ... }
|
||||
if device.ParserType == "redfish" { ... }
|
||||
```
|
||||
|
||||
Если нужен маппинг — он живёт в конфиге или справочной таблице БД, не в коде:
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
vendor_parsers:
|
||||
dell: redfish
|
||||
hp: ilo
|
||||
cisco: ucs
|
||||
```
|
||||
|
||||
```sql
|
||||
-- справочник в БД
|
||||
SELECT parser_type FROM vendor_registry WHERE LOWER(vendor) = LOWER(?);
|
||||
```
|
||||
|
||||
---
|
||||
- Запрещены сравнения и switch по имени вендора (`if device.Vendor == "Dell"`) и
|
||||
списки вендоров в коде (`var knownVendors = []string{...}`).
|
||||
- Логика определяется по полям из данных (`device.HasIPMI`, `device.ParserType`),
|
||||
не по названию вендора.
|
||||
- Если нужен маппинг вендор → поведение, он живёт в конфиге или справочной таблице БД.
|
||||
|
||||
## Исключения
|
||||
|
||||
@@ -70,8 +29,6 @@ SELECT parser_type FROM vendor_registry WHERE LOWER(vendor) = LOWER(?);
|
||||
|
||||
В этих местах название вендора — идентификатор модуля, не условие в логике.
|
||||
|
||||
---
|
||||
|
||||
## Почему
|
||||
|
||||
Хардкод вендора делает код хрупким: новый вендор требует правок в коде, а не в данных.
|
||||
|
||||
46
rules/patterns/vendor-installer-verification/README.md
Normal file
46
rules/patterns/vendor-installer-verification/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Vendor Installer Verification Pattern Notes
|
||||
|
||||
This file keeps examples. The normative rules live in `contract.md`.
|
||||
|
||||
## Download Order
|
||||
|
||||
```sh
|
||||
BASE_URL="https://vendor.example.com/downloads/${VERSION}"
|
||||
BIN_FILE="/var/cache/vendor-${VERSION}.run"
|
||||
SHA_FILE="/var/cache/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 1. Download checksum first
|
||||
wget -q -O "$SHA_FILE" "${BASE_URL}/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 2. Download installer
|
||||
wget --show-progress -O "$BIN_FILE" "${BASE_URL}/vendor-${VERSION}.run"
|
||||
|
||||
# 3. Verify
|
||||
cd /var/cache
|
||||
sha256sum -c "$SHA_FILE" || { echo "ERROR: sha256 mismatch"; rm -f "$BIN_FILE"; exit 1; }
|
||||
```
|
||||
|
||||
## Cache with Verification
|
||||
|
||||
```sh
|
||||
verify_cached() {
|
||||
[ -s "$SHA_FILE" ] || return 1 # sha256 file missing or empty
|
||||
[ -s "$BIN_FILE" ] || return 1 # binary missing or empty
|
||||
cd "$(dirname "$BIN_FILE")"
|
||||
sha256sum -c "$SHA_FILE" --status 2>/dev/null
|
||||
}
|
||||
|
||||
if ! verify_cached; then
|
||||
rm -f "$BIN_FILE" "$SHA_FILE"
|
||||
# ... download and verify
|
||||
else
|
||||
echo "verified from cache"
|
||||
fi
|
||||
```
|
||||
|
||||
## Version Validation
|
||||
|
||||
```sh
|
||||
curl -sIL "https://vendor.example.com/downloads/${VERSION}/installer.run" \
|
||||
| grep -i 'http/\|content-length'
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contract: Vendor Installer Verification
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -8,75 +8,19 @@ Rules for downloading and verifying proprietary vendor installers (`.run`, `.exe
|
||||
where the vendor publishes a checksum alongside the binary.
|
||||
Applies to: NVIDIA drivers, vendor CLI tools, firmware packages.
|
||||
|
||||
---
|
||||
|
||||
## Download Order
|
||||
|
||||
Always download the checksum file **before** the installer:
|
||||
|
||||
```sh
|
||||
BASE_URL="https://vendor.example.com/downloads/${VERSION}"
|
||||
BIN_FILE="/var/cache/vendor-${VERSION}.run"
|
||||
SHA_FILE="/var/cache/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 1. Download checksum first
|
||||
wget -q -O "$SHA_FILE" "${BASE_URL}/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 2. Download installer
|
||||
wget --show-progress -O "$BIN_FILE" "${BASE_URL}/vendor-${VERSION}.run"
|
||||
|
||||
# 3. Verify
|
||||
cd /var/cache
|
||||
sha256sum -c "$SHA_FILE" || { echo "ERROR: sha256 mismatch"; rm -f "$BIN_FILE"; exit 1; }
|
||||
```
|
||||
|
||||
Reason: if the download is interrupted, you have the expected checksum to verify against on retry.
|
||||
|
||||
---
|
||||
|
||||
## Cache with Verification
|
||||
|
||||
Never assume a cached file is valid — a previous download may have been interrupted (0-byte file):
|
||||
|
||||
```sh
|
||||
verify_cached() {
|
||||
[ -s "$SHA_FILE" ] || return 1 # sha256 file missing or empty
|
||||
[ -s "$BIN_FILE" ] || return 1 # binary missing or empty
|
||||
cd "$(dirname "$BIN_FILE")"
|
||||
sha256sum -c "$SHA_FILE" --status 2>/dev/null
|
||||
}
|
||||
|
||||
if ! verify_cached; then
|
||||
rm -f "$BIN_FILE" "$SHA_FILE"
|
||||
# ... download and verify
|
||||
else
|
||||
echo "verified from cache"
|
||||
fi
|
||||
```
|
||||
|
||||
**Never check only for file existence.** Check that the file is non-empty (`-s`) AND passes checksum.
|
||||
|
||||
---
|
||||
|
||||
## Version Validation
|
||||
|
||||
Before writing build scripts, verify the version URL actually exists:
|
||||
|
||||
```sh
|
||||
curl -sIL "https://vendor.example.com/downloads/${VERSION}/installer.run" \
|
||||
| grep -i 'http/\|content-length'
|
||||
```
|
||||
|
||||
A `404` or `content-length: 0` means the version does not exist on that CDN.
|
||||
Vendor version numbering may have gaps (e.g. NVIDIA skips minor versions on some CDNs).
|
||||
|
||||
---
|
||||
See `README.md` for shell snippets.
|
||||
|
||||
## Rules
|
||||
|
||||
- Download checksum before installer — never after.
|
||||
- Download the checksum file **before** the installer — never after. If the download is
|
||||
interrupted, you still have the expected checksum to verify against on retry.
|
||||
- Verify checksum before extracting or executing.
|
||||
- On mismatch: delete the file, exit with error. Never proceed with a bad installer.
|
||||
- Never assume a cached file is valid — a previous download may have been interrupted.
|
||||
**Never check only for file existence**: the file must be non-empty (`-s`) AND pass checksum.
|
||||
- Cache by `version` + any secondary key (e.g. kernel version for compiled modules).
|
||||
- Before writing build scripts, verify the version URL actually exists (`curl -sIL`).
|
||||
A `404` or `content-length: 0` means the version is absent on that CDN; vendor version
|
||||
numbering may have gaps.
|
||||
- Never commit installer files to git — always download at build time.
|
||||
- Log the expected hash when downloading so failures are diagnosable.
|
||||
|
||||
Reference in New Issue
Block a user