@7n/rules-ci-azure 1.4.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0] - 2026-07-27
4
+
5
+ ### Added
6
+
7
+ - Generic ci.artifact@1 consumer: contains-step перевірка lint-кроку Azure Pipelines на будь-якій глибині, diagnostic-only
8
+
9
+ ### Changed
10
+
11
+ - Маніфест плагіна переведено на universal slot bus (`requiresPluginApi: 2`, `slots.provides` з `rules.directory@1` замість `contributes.rules`) — spec 2026-07-27-universal-plugin-slots-lang-php-extraction, Фаза 2.
12
+
13
+ ### Fixed
14
+
15
+ - Concern `ci_artifact/consume` перейменовано на `ci_artifact/consume_azure` — колізія імені з
16
+ однойменним `consume`-концерном `@7n/rules-ci-github` глушила Azure-перевірку (перший власник
17
+ виграє за іменем концерну), коли обидва CI-плагіни активні в одному consumer-репо
18
+ (spec 2026-07-27-universal-plugin-slots-lang-php-extraction, Фаза 5 parity battery)
19
+
20
+ ### Removed
21
+
22
+ - PHP-specific mixin-концерн `rules/php/lint_pipeline_php/**` видалено (Фаза 4 spec universal-plugin-slots-lang-php-extraction) — Azure lint-степ `php` тепер обслуговує generic `ci.artifact@1` consumer (contains-step, diagnostic-only) через contribution `@7n/rules-lang-php`
23
+
3
24
  ## [1.4.8] - 2026-07-24
4
25
 
5
26
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-ci-azure",
3
- "version": "1.4.8",
3
+ "version": "2.0.0",
4
4
  "description": "Плагін @7n/rules: канон Azure Pipelines (azure-pipelines.yml) — структура, lint-степ, VS Code",
5
5
  "keywords": [
6
6
  "azure-pipelines",
@@ -22,17 +22,24 @@
22
22
  "type": "module",
23
23
  "files": [
24
24
  "rules",
25
+ "slots",
25
26
  "CHANGELOG.md",
26
27
  "!**/*.test.mjs",
27
28
  "!**/*_test.rego",
28
29
  "!**/fixtures/**"
29
30
  ],
30
31
  "n-rules": {
32
+ "requiresPluginApi": 2,
31
33
  "capabilities": [
32
34
  "ci:azure"
33
35
  ],
34
- "contributes": {
35
- "rules": true
36
+ "slots": {
37
+ "provides": [
38
+ { "slot": "rules.directory", "version": 1, "id": "azure-rules", "resource": "./rules" }
39
+ ],
40
+ "consumes": [
41
+ { "slot": "ci.artifact", "versions": [1], "handler": "./slots/ci-artifact-consumer.mjs" }
42
+ ]
36
43
  }
37
44
  },
38
45
  "peerDependencies": {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Тонка обгортка над `collectCiArtifactContributions`
3
+ * (`@7n/rules/scripts/lib/ci-artifact-collect.mjs`) з capability цього consumer-а (spec
4
+ * `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.3, Фаза 3). Спільна логіка
5
+ * (collect + collision-детекція) живе в `@7n/rules`'s `ci-artifact-collect.mjs` — той самий
6
+ * контракт, що й `@7n/rules-ci-github`, розрізняючись лише capability-фільтром.
7
+ */
8
+ import { collectCiArtifactContributions } from '@7n/rules/scripts/lib/ci-artifact-collect.mjs'
9
+
10
+ /** Capability, для якої релевантні contributions цього consumer-а. */
11
+ const TARGET_CAPABILITY = 'ci:azure'
12
+
13
+ /**
14
+ * @typedef {import('@7n/rules/scripts/lib/slot-contracts-ci.mjs').CiArtifactCandidate} ArtifactCandidate
15
+ */
16
+
17
+ /**
18
+ * @param {string} cwd абсолютний корінь consumer-репо
19
+ * @returns {ReturnType<typeof collectCiArtifactContributions>} результат {@link collectCiArtifactContributions} для `ci:azure`
20
+ */
21
+ export function collectArtifacts(cwd) {
22
+ return collectCiArtifactContributions(cwd, TARGET_CAPABILITY)
23
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "lint": { "scope": "full" }
5
+ }
@@ -0,0 +1,21 @@
1
+ ## Generic `ci.artifact@1` consumer (Azure Pipelines)
2
+
3
+ Правило перевіряє CI-артефакти, які постачають language-плагіни (напр. майбутній
4
+ `@7n/rules-lang-php`) через universal typed slot `ci.artifact@1` (mixin `@7n/rules-ci-azure`) —
5
+ без жодного PHP чи іншого мовного literal у цьому пакеті.
6
+
7
+ ### Поведінка
8
+
9
+ `mergeStrategy: "contains-step"`: серед `script`-кроків `azure-pipelines.yml` на БУДЬ-ЯКІЙ
10
+ глибині (`steps`/`jobs`/`stages` вкладені довільно) має бути запуск canonical-команди
11
+ (`n-rules <cmd>` або `@7n/rules <cmd>`, `<cmd>` — з `template` contribution-у) АБО загальний
12
+ `n-rules lint --no-fix --full` / `@7n/rules lint --no-fix --full` (повний прогін покриває всі
13
+ домени — окремий крок не потрібен). Обов'язковий read-only marker `--no-fix` десь у тому самому
14
+ script-блобі.
15
+
16
+ `mode: "patch-existing"` — `azure-pipelines.yml` належить окремому, самостійному концерну
17
+ (структура pipeline-файлу); його відсутність — НЕ порушення цього generic-у, перевірка мовчки
18
+ пропускається.
19
+
20
+ v1 — diagnostic-only: payload `fix` завжди `false` (жоден consumer не приймає
21
+ `fix: true`-descriptor для `mergeStrategy: "contains-step"`), T0-фіксу немає — лише violation.
@@ -0,0 +1,22 @@
1
+ ---
2
+ type: JS Module
3
+ title: collect-artifacts.mjs
4
+ resource: plugins/ci-azure/rules/ci_artifact/consume_azure/collect-artifacts.mjs
5
+ docgen:
6
+ crc: 74241d83
7
+ ---
8
+
9
+ Тонка обгортка над `collectCiArtifactContributions` (`@7n/rules/scripts/lib/ci-artifact-collect.mjs`) з capability цього consumer-а (spec `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.3). Спільна логіка (collect + collision-детекція) живе в `@7n/rules`'s `ci-artifact-collect.mjs` — той самий контракт, що й `@7n/rules-ci-github`, розрізняючись лише capability-фільтром.
10
+
11
+ ## Поведінка
12
+
13
+ `collectArtifacts(cwd)` викликає `collectCiArtifactContributions(cwd, 'ci:azure')`.
14
+
15
+ ## Публічний API
16
+
17
+ `collectArtifacts` — повертає `{ relevant, collisions, errors }` для `ci:azure`.
18
+
19
+ ## Гарантії поведінки
20
+
21
+ * **Один consumer — одна capability**: `TARGET_CAPABILITY = 'ci:azure'` — фіксоване значення.
22
+ * **Той самий контракт, що й GitHub-consumer**: обидва просто передають різну capability у спільний `collectCiArtifactContributions`.
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/ci-azure/rules/ci_artifact/consume_azure
4
+ resource: plugins/ci-azure/rules/ci_artifact/consume_azure/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | --------------------------------------------- | --------- |
9
+ | [collect-artifacts.mjs](collect-artifacts.md) | JS Module |
10
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,39 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/ci-azure/rules/ci_artifact/consume_azure/main.mjs
5
+ docgen:
6
+ crc: 8cefa299
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 100
10
+ issues: judge-refine:kept-original,judge:inaccurate:0.98
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Для активного `ci:azure` у `package.json` знаходить придатний до виконання опис артефактів і одразу показує проблеми колекції, якщо вони є. Далі для кожного артефакта з цього опису звіряє вміст цільового файлу з вимогами перевірки та у `lint` повідомляє лише про фактичні невідповідності.
17
+
18
+ ## Поведінка
19
+
20
+ 1. Перевіряє, чи для активного `ci:azure` у `package.json` є придатний до виконання опис артефактів, і одразу фіксує проблеми колекції, якщо вони є.
21
+ 2. Для кожного релевантного артефакта звіряє очікувану поведінку з поточним вмістом цільового файлу та позначає лише реальні невідповідності.
22
+ 3. Якщо для внеску неможливо відновити канонічну команду, повідомляє про шаблонну помилку на цільовому файлі й не продовжує перевірку цього випадку.
23
+ 4. Якщо перевірка не застосовується до поточного файлу, мовчки пропускає його; це свідомо покриває ситуацію, коли потрібний pipeline-файл лежить в іншому concern і його відсутність не є порушенням тут.
24
+ 5. Повертає підсумок перевірки через `lint` без змін у файловій системі чи базі даних.
25
+
26
+ ## Публічний API
27
+
28
+ - lint — Detector generic-consumer-а слоту `ci.artifact@1` для `ci:azure` (spec §7.3, Фаза 3):
29
+ перевіряє наявність канонічного lint-кроку (АБО загального full-lint fallback-у) на будь-якій
30
+ глибині `azure-pipelines.yml` — без жодного PHP/lang-specific literal тут (v1 diagnostic-only,
31
+ `fix: false` — немає T0-фіксу, лише порушення).
32
+
33
+ ## Сценарії використання
34
+
35
+ - `plugins/ci-azure/rules/ci_artifact/consume_azure/tests/consume.test.mjs` (ci-azure ci.artifact consumer) — patch-existing: pipeline відсутній → 0 violations (окремий концерн azure-pipelines); contains-step: canonical крок відсутній на будь-якій глибині → violation; contains-step: canonical крок на глибині stages→jobs→steps знаходиться; contains-step: canonical крок на голому jobs→steps (без stages) знаходиться; приймає загальний full-lint fallback замість domain-команди; ще 4
36
+
37
+ ## Гарантії поведінки
38
+
39
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -0,0 +1,68 @@
1
+ /** @see ./docs/main.md */
2
+ import { reportCiArtifactCollectionDiagnostics } from '@7n/rules/scripts/lib/ci-artifact-collect.mjs'
3
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
4
+
5
+ // `slots/` — публікується разом з пакетом (spec §7.3); `package.json#files` цього плагіна
6
+ // доповнить оркестратор інтеграції (Фаза 3 явно НЕ чіпає plugin manifests, spec-задача).
7
+ import { diagnoseArtifact, loadCanonicalCommand } from '../../../slots/ci-artifact-consumer.mjs'
8
+ import { collectArtifacts } from './collect-artifacts.mjs'
9
+
10
+ /**
11
+ * Діагностує ОДИН artifact (`mergeStrategy: "contains-step"`) проти поточного стану
12
+ * `targetPath`. Винесено з {@link lint} — та сама причина.
13
+ * @param {ReturnType<typeof createViolationReporter>} reporter репортер порушень
14
+ * @param {{ cwd: string }} ctx контекст лінту (лише `cwd` тут потрібен)
15
+ * @param {import('./collect-artifacts.mjs').ArtifactCandidate} candidate одна валідна (без колізій) contribution
16
+ * @returns {Promise<void>}
17
+ */
18
+ async function reportArtifact(reporter, ctx, { contribution, descriptor }) {
19
+ const commandResult = await loadCanonicalCommand(contribution, descriptor)
20
+ if (!commandResult.ok) {
21
+ reporter.fail(`ci.artifact "${descriptor.artifactId}" (${contribution.pluginName}): ${commandResult.reason}`, {
22
+ reason: 'template-error',
23
+ file: descriptor.targetPath
24
+ })
25
+ return
26
+ }
27
+
28
+ const diag = await diagnoseArtifact({
29
+ cwd: ctx.cwd,
30
+ targetPath: descriptor.targetPath,
31
+ command: commandResult.command
32
+ })
33
+ // patch-existing: pipeline-файл належить ІНШОМУ, окремому концерну (azure-pipelines) — його
34
+ // відсутність не порушення ЦЬОГО concern-а, generic consumer мовчки пропускає (spec §7.1).
35
+ if (!diag.applicable) return
36
+
37
+ for (const v of diag.violations) {
38
+ reporter.fail(
39
+ `${descriptor.targetPath}: ${v} (contribution "${descriptor.artifactId}" від ${contribution.pluginName})`,
40
+ {
41
+ reason: 'artifact-mismatch',
42
+ file: descriptor.targetPath,
43
+ data: {
44
+ kind: 'artifact-mismatch',
45
+ artifactId: descriptor.artifactId,
46
+ contributorPlugin: contribution.pluginName,
47
+ contributionId: contribution.id
48
+ }
49
+ }
50
+ )
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Detector generic-consumer-а слоту `ci.artifact@1` для `ci:azure` (spec §7.3, Фаза 3):
56
+ * перевіряє наявність канонічного lint-кроку (АБО загального full-lint fallback-у) на будь-якій
57
+ * глибині `azure-pipelines.yml` — без жодного PHP/lang-specific literal тут (v1 diagnostic-only,
58
+ * `fix: false` — немає T0-фіксу, лише порушення).
59
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
60
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки
61
+ */
62
+ export async function lint(ctx) {
63
+ const reporter = createViolationReporter(ctx)
64
+ const collected = await collectArtifacts(ctx.cwd)
65
+ reportCiArtifactCollectionDiagnostics(reporter, collected)
66
+ for (const candidate of collected.relevant) await reportArtifact(reporter, ctx, candidate)
67
+ return reporter.result()
68
+ }
@@ -0,0 +1 @@
1
+ { "auto": "завжди" }
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Generic-споживач ci.artifact@1 — CI-артефакти, які декларують активні мовні плагіни (required-file/patch-existing), матеріалізуються й перевіряються CI-плагіном провайдера
3
+ version: '1.0'
4
+ alwaysApply: false
5
+ ---
6
+
7
+ Правило `ci_artifact` — generic-споживач slot-у `ci.artifact@1` (spec
8
+ `2026-07-27-universal-plugin-slots-lang-php-extraction`). Мовні плагіни (напр.
9
+ `@7n/rules-lang-php`) декларують CI-артефакти декларативно — descriptor + template; це правило
10
+ матеріалізує їх для свого CI-провайдера: перевіряє наявність/канонічність цільових файлів
11
+ (`required-file`), домерджує provider-specific патчі до чинних generic-артефактів
12
+ (`patch-existing`) і, де дозволено descriptor-ом (`fix: true`), виконує детермінований T0-фікс.
13
+
14
+ Правило активне завжди при встановленому CI-плагіні: без активних contributions воно — тихий
15
+ no-op (zero-touch), тому мовний плагін не потребує CI-плагіна і навпаки. Колізія `artifactId`
16
+ між двома плагінами — помилка з provenance обох сторін, без тихого перезапису.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Generic consumer слоту `ci.artifact@1` для `@7n/rules-ci-azure` (spec
3
+ * `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.3, Фаза 3).
4
+ *
5
+ * `mergeStrategy: "contains-step"` — Azure Pipelines YAML — на відміну від GitHub-adapter-а
6
+ * (structural deep merge) тут перевіряється лише НАЯВНІСТЬ канонічного lint-кроку десь у дереві
7
+ * (`steps`/`jobs`/`stages` на будь-якій глибині — топологія Azure pipeline вкладена довільно:
8
+ * `stages[].jobs[].steps[]`, `jobs[].steps[]`, голий `steps[]`). Канонічна команда (з
9
+ * `template`) АБО загальний full-lint (`n-rules lint --no-fix --full` /
10
+ * `@7n/rules lint --no-fix --full`, provider-level policy — той самий fallback, що вже є у
11
+ * чинному `lint_pipeline_php.rego`) — обидва варіанти покривають concern; обов'язковий
12
+ * read-only marker `--no-fix` десь у тому самому script-блобі.
13
+ *
14
+ * `template` — YAML з одним полем `script` (canonical command WITHOUT `--no-fix`, напр.
15
+ * `n-rules lint php`); provider сам конструює `n-rules <cmd>`/`@7n/rules <cmd>` варіанти —
16
+ * бінарний prefix є provider-know-how, не частиною descriptor-а (spec §2 рішення Б: лише
17
+ * consumer-adapter матеріалізує contribution).
18
+ *
19
+ * v1 — DIAGNOSTIC-ONLY: `fix: false` у payload (spec §7.3, "у v1 зберігає чинну поведінку
20
+ * fix: false") — модуль НЕ експортує T0 apply-функцію; consumer лише детектує.
21
+ *
22
+ * Default export сумісний з `loadSlotConsumer` (spec §3.3): стабільний `id` + `validate(payload)`.
23
+ */
24
+ import { existsSync, readFileSync } from 'node:fs'
25
+ import { join } from 'node:path'
26
+
27
+ import { resolveArtifactTemplatePath, validateCiArtifactPayload } from '@7n/rules/plugin-api'
28
+
29
+ /** Provider-level fallback: загальний full-lint покриває будь-який домен (спека §7.3, як у lint_pipeline_php.rego). */
30
+ const FULL_LINT_MARKERS = ['n-rules lint --no-fix --full', '@7n/rules lint --no-fix --full']
31
+ const NO_FIX_MARKER = '--no-fix'
32
+
33
+ /**
34
+ * Збирає всі `script:`-текстові поля Azure pipeline на будь-якій глибині (`steps`/`jobs`/`stages`
35
+ * вкладені довільно — обходимо ВЕСЬ document tree, а не лише відомі ключі, той самий підхід, що
36
+ * `lint_pipeline_php.rego`'s `walk(input, [_, node])`).
37
+ * @param {unknown} node поточний вузол дерева
38
+ * @param {string[]} out акумулятор script-текстів (мутується)
39
+ * @returns {void}
40
+ */
41
+ function collectScripts(node, out) {
42
+ if (Array.isArray(node)) {
43
+ for (const el of node) collectScripts(el, out)
44
+ return
45
+ }
46
+ if (node === null || typeof node !== 'object') return
47
+ if (typeof node.script === 'string') out.push(node.script)
48
+ for (const v of Object.values(node)) collectScripts(v, out)
49
+ }
50
+
51
+ /**
52
+ * Провайдер-варіанти canonical-команди (`n-rules <cmd>` / `@7n/rules <cmd>`) — ім'я бінарника
53
+ * лишається provider-know-how, не частиною descriptor-а.
54
+ * @param {string} command суфікс команди з template (`script`-поле, без `--no-fix`)
55
+ * @returns {string[]} варіанти маркера
56
+ */
57
+ function commandVariants(command) {
58
+ return [`n-rules ${command}`, `@7n/rules ${command}`]
59
+ }
60
+
61
+ /**
62
+ * Читає й парсить canonical template (YAML `{ script: "<domain command>" }`) contribution-у за
63
+ * безпечним шляхом.
64
+ * @param {{ packageRoot: string, resourcePath: string | null }} contribution provenance
65
+ * @param {import('@7n/rules/plugin-api').CiArtifactDescriptor} descriptor валідований payload
66
+ * @returns {Promise<{ ok: true, command: string } | { ok: false, reason: string }>} результат
67
+ */
68
+ export async function loadCanonicalCommand(contribution, descriptor) {
69
+ const resolved = resolveArtifactTemplatePath(contribution, descriptor)
70
+ if (!resolved.ok) return { ok: false, reason: resolved.reason }
71
+ if (!resolved.exists) return { ok: false, reason: `template не знайдено (${resolved.absPath})` }
72
+ const { parse } = await import('yaml')
73
+ const parsed = parse(readFileSync(resolved.absPath, 'utf8'))
74
+ if (!parsed || typeof parsed.script !== 'string' || parsed.script === '') {
75
+ return { ok: false, reason: `template "${resolved.absPath}" має мати непорожнє поле "script"` }
76
+ }
77
+ return { ok: true, command: parsed.script }
78
+ }
79
+
80
+ /**
81
+ * Діагностує один `ci.artifact@1` artifact (mergeStrategy `contains-step`) проти поточного
82
+ * стану `targetPath` (`patch-existing`-семантика: файл, якого немає, — no-op, spec §7.1 —
83
+ * pipeline-файл належить іншому, окремому концерну azure-pipelines, не цьому generic-у).
84
+ * @param {{ cwd: string, targetPath: string, command: string }} args вхід
85
+ * @returns {{ applicable: boolean, violations: string[] }} `applicable: false` — файл відсутній, generic concern мовчки пропускає
86
+ */
87
+ export async function diagnoseArtifact({ cwd, targetPath, command }) {
88
+ const absTarget = join(cwd, targetPath)
89
+ if (!existsSync(absTarget)) return { applicable: false, violations: [] }
90
+ const { parse } = await import('yaml')
91
+ let doc
92
+ try {
93
+ doc = parse(readFileSync(absTarget, 'utf8'))
94
+ } catch {
95
+ return { applicable: true, violations: [`${targetPath}: не парситься як YAML`] }
96
+ }
97
+ const scripts = []
98
+ collectScripts(doc, scripts)
99
+ const blob = scripts.join('\n')
100
+
101
+ const domainMarkers = commandVariants(command)
102
+ const hasLintStep = [...domainMarkers, ...FULL_LINT_MARKERS].some(m => blob.includes(m))
103
+ if (!hasLintStep) {
104
+ return {
105
+ applicable: true,
106
+ violations: [`${targetPath}: має бути script-крок з "${command}" або full-lint (--no-fix --full)`]
107
+ }
108
+ }
109
+ if (!blob.includes(NO_FIX_MARKER)) {
110
+ return { applicable: true, violations: [`${targetPath}: lint-крок має запускатись з "${NO_FIX_MARKER}"`] }
111
+ }
112
+ return { applicable: true, violations: [] }
113
+ }
114
+
115
+ /** `loadSlotConsumer`-сумісний default export (spec §3.3): стабільний `id` + `validate(payload)`. */
116
+ export default {
117
+ id: 'ci-azure-artifact',
118
+ /**
119
+ * @param {unknown} payload сирий (розпарсений) `ci.artifact@1` payload
120
+ * @returns {boolean} true — валідний payload; v1 додатково вимагає `mergeStrategy: "contains-step"` та `fix: false` (diagnostic-only, spec §7.3)
121
+ */
122
+ validate(payload) {
123
+ const result = validateCiArtifactPayload(payload)
124
+ if (!result.ok) return false
125
+ return result.descriptor.mergeStrategy === 'contains-step' && result.descriptor.fix === false
126
+ }
127
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: JS Module
3
+ title: ci-artifact-consumer.mjs
4
+ resource: plugins/ci-azure/slots/ci-artifact-consumer.mjs
5
+ docgen:
6
+ crc: 68401129
7
+ ---
8
+
9
+ Generic consumer слоту `ci.artifact@1` для `@7n/rules-ci-azure` (spec `2026-07-27-universal-plugin-slots-lang-php-extraction`, §7.3). Перевіряє Azure Pipelines lint-кроки будь-якого language-плагіна — без жодного PHP чи іншого мовного literal у цьому пакеті.
10
+
11
+ ## Поведінка
12
+
13
+ `mergeStrategy: "contains-step"` — на відміну від GitHub-adapter-а (structural deep merge) тут перевіряється лише наявність канонічного lint-кроку десь у дереві (`steps`/`jobs`/`stages` на будь-якій глибині — топологія Azure pipeline вкладена довільно: `stages[].jobs[].steps[]`, `jobs[].steps[]`, голий `steps[]`). Канонічна команда (з `template`) АБО загальний full-lint (`n-rules lint --no-fix --full` / `@7n/rules lint --no-fix --full`, той самий provider-level fallback, що вже є у чинному `lint_pipeline_php.rego`) — обидва варіанти покривають concern; обов'язковий read-only marker `--no-fix` десь у тому самому script-блобі.
14
+
15
+ `template` — YAML з одним полем `script` (canonical command БЕЗ `--no-fix`, напр. `n-rules lint php`); provider сам конструює `n-rules <cmd>`/`@7n/rules <cmd>` варіанти — бінарний префікс лишається provider-know-how, не частиною descriptor-а.
16
+
17
+ 1. **`loadCanonicalCommand(contribution, descriptor)`** резолвить безпечний шлях `template`, читає й парсить `{ script }`.
18
+ 2. **`diagnoseArtifact({ cwd, targetPath, command })`** обходить весь document tree (`collectScripts`), збирає всі `script`-поля в один текстовий рядок і перевіряє наявність canonical/fallback маркера та `--no-fix`. Файл відсутній → `{ applicable: false, violations: [] }` (`patch-existing`-семантика: pipeline-файл належить окремому концерну, spec §7.1).
19
+
20
+ v1 — diagnostic-only: модуль не експортує T0 apply-функцію (`fix: false` у payload).
21
+
22
+ ## Публічний API
23
+
24
+ `loadCanonicalCommand` — читає й парсить canonical-команду з template одного artifact-у.
25
+ `diagnoseArtifact` — діагностує наявність canonical/fallback lint-кроку й `--no-fix`.
26
+ Default export — `loadSlotConsumer`-сумісний `{ id: 'ci-azure-artifact', validate(payload) }`; `validate` додатково вимагає `mergeStrategy: "contains-step"` і `fix: false`.
27
+
28
+ ## Гарантії поведінки
29
+
30
+ * **Будь-яка глибина**: `collectScripts` обходить ВЕСЬ document tree рекурсивно, не покладаючись на конкретні ключі (`steps`/`jobs`/`stages`).
31
+ * **Fallback завжди доступний**: загальний full-lint команда приймається замість domain-specific — окремий per-domain крок не обов'язковий.
32
+ * **Diagnostic-only**: жодного T0-фіксу в v1 — лише violation-повідомлення.
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/ci-azure/slots
4
+ resource: plugins/ci-azure/slots/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | --- | --- |
9
+ | [ci-artifact-consumer.mjs](ci-artifact-consumer.md) | JS Module |
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "policy": {
4
- "files": {
5
- "single": "azure-pipelines.yml",
6
- "required": true
7
- },
8
- "missingMessage": "azure-pipelines.yml не існує — додай lint-степ `bunx n-rules lint php --no-fix` (azure-pipelines.mdc)"
9
- }
10
- }
@@ -1,13 +0,0 @@
1
- ## Azure Pipelines: обов'язковий lint-степ `php`
2
-
3
- Rego-пакет: `php.lint_pipeline_php` (mixin з плагіна `@7n/rules-ci-azure`)
4
-
5
- **Цільовий файл:** `azure-pipelines.yml`
6
-
7
- Серед `script`-кроків pipeline (будь-яка глибина — steps/jobs/stages) сукупно мають бути substrings `n-rules lint php` (приймається і `npx @7n/rules lint php`) та `--no-fix`. Це провайдер-аналог GitHub-workflow `lint-php.yml` з `@7n/rules-ci-github`: намір «CI-lint домену `php`» однаковий, реалізація — своя для кожного CI-провайдера.
8
-
9
- ```yaml
10
- steps:
11
- - script: bunx n-rules lint php --no-fix
12
- displayName: Lint php
13
- ```
@@ -1,39 +0,0 @@
1
- # Обов'язковий lint-степ домену `php` у `azure-pipelines.yml` (провайдер-аналог
2
- # GitHub-workflow `lint-php.yml` з @7n/rules-ci-github): серед script-кроків pipeline
3
- # на будь-якій глибині має бути запуск `n-rules lint php` (або `@7n/rules lint php`)
4
- # з `--no-fix`. Mixin-концерн правила `php` — активний лише разом із правилом.
5
- #
6
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
7
- # (conftest.mdc). Лінт — `n-rules lint rego` (regal).
8
- package php.lint_pipeline_php
9
-
10
- import rego.v1
11
-
12
- scripts contains s if {
13
- walk(input, [_, node])
14
- is_object(node)
15
- s := node.script
16
- is_string(s)
17
- }
18
-
19
- script_blob := concat("\n", [s | some s in scripts])
20
-
21
- has_lint_step if contains(script_blob, "n-rules lint php")
22
-
23
- has_lint_step if contains(script_blob, "@7n/rules lint php")
24
-
25
- # Загальний full-прогін покриває всі домени — окремий php-степ не потрібен.
26
- has_lint_step if contains(script_blob, "n-rules lint --no-fix --full")
27
-
28
- has_lint_step if contains(script_blob, "@7n/rules lint --no-fix --full")
29
-
30
- deny contains msg if {
31
- not has_lint_step
32
- msg := "azure-pipelines.yml: має бути script-крок `n-rules lint php --no-fix` (azure-pipelines.mdc)"
33
- }
34
-
35
- deny contains msg if {
36
- has_lint_step
37
- not contains(script_blob, "--no-fix")
38
- msg := "azure-pipelines.yml: lint-степ `php` має запускатись з `--no-fix` (CI без мутацій, azure-pipelines.mdc)"
39
- }