fix(support-guard): не блокировать автономные внешние обработки/отчёты (#39)

При поиске корня конфигурации guard поднимался по дереву вверх и «проскакивал»
собственный корень автономной внешней обработки/отчёта (ExternalDataProcessor /
ExternalReport), лежащей внутри дерева выгрузки конфигурации. Если у охватывающей
конфигурации выключена возможность изменения (G=1), внешний объект ложно
блокировался как «объект типовой конфигурации на поддержке», а info-навыки
выводили нерелевантную строку «Поддержка: конфигурация read-only».

Теперь climb останавливается на границе автономного объекта: если целевой файл или
встреченный по пути <каталог>.xml имеет корень ExternalDataProcessor/ExternalReport,
подъём прекращается и объект не привязывается к конфигурации. Корень внешнего объекта
всегда глубже Configuration.xml, поэтому встречается первым — регрессии для обычных
объектов конфигурации нет.

Синхронно во всех копиях guard-а (навыки автономны): хук support-state.mjs
(decideSupport + findConfigRoot), 16 мутаторов (Assert-EditAllowed), 5 info-навыков
и meta-info (Get-SupportStatusForPath / Get-ObjectSupportStatus) — ps1 и py. Для
info-навыков строка «Поддержка:» для внешнего объекта опускается.

Тесты: hooks/test/run.mjs — секция внешней границы (G=1 + встроенная EPF);
tests/skills — кейсы mxl-compile (guard пропускает) и mxl-info (строка опущена).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-17 19:46:54 +03:00
co-authored by Claude Opus 4.8
parent e7b5df4d50
commit ddc1641176
64 changed files with 1400 additions and 58 deletions
+24 -1
View File
@@ -1,4 +1,4 @@
// support-state.mjs v1.0 — decode 1C support state (Ext/ParentConfigurations.bin) for Claude Code hooks
// support-state.mjs v1.1 — decode 1C support state (Ext/ParentConfigurations.bin) for Claude Code hooks
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
//
// Canonical port of the in-skill guard Assert-EditAllowed / assert_edit_allowed
@@ -13,6 +13,20 @@ import { readFileSync, existsSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
const GUID_RE = /\buuid="([0-9a-fA-F-]{36})"/;
// Root child of an autonomous external object dump (EPF/ERF). Such an object is never
// part of a configuration on support, even when its dump sits inside the config tree —
// the climb must stop at this boundary instead of attributing it to the enclosing config.
const EXT_ROOT_RE = /<MetaDataObject\b[^>]*>\s*<(?:\w+:)?(?:ExternalDataProcessor|ExternalReport)\b/;
// True when xmlPath is the root file of an autonomous ExternalDataProcessor / ExternalReport.
export function isExternalObjectRoot(xmlPath) {
try {
if (!existsSync(xmlPath) || !statSync(xmlPath).isFile()) return false;
return EXT_ROOT_RE.test(readFileSync(xmlPath, 'utf8').slice(0, 1000));
} catch {
return false;
}
}
// First uuid="..." in an object XML == root element uuid (the <MetaDataObject> wrapper
// carries none), matching the reference's "first element child uuid" semantics.
@@ -40,7 +54,11 @@ export function findConfigRoot(startPath) {
} catch {
d = dirname(startPath);
}
// startPath itself may be the external object root file.
if (isExternalObjectRoot(startPath)) return { cfgDir: null, binPath: null, isExtension: false };
for (let i = 0; i < 12 && d; i++) {
// Crossed the boundary of an autonomous external object → not part of any config.
if (isExternalObjectRoot(d + '.xml')) return { cfgDir: null, binPath: null, isExtension: false };
const cand = join(d, 'Ext', 'ParentConfigurations.bin');
const cfgX = join(d, 'Configuration.xml');
if (existsSync(cand) || existsSync(cfgX)) {
@@ -70,6 +88,9 @@ export function findConfigRoot(startPath) {
export function decideSupport(targetPath, require = 'editable') {
const result = { blocked: false, reason: '', code: null, cfgDir: null, targetPath };
try {
// Autonomous external object (EPF/ERF): its root is never part of a configuration on
// support, even when the dump sits inside the config tree — do not attribute it (issue #39).
if (isExternalObjectRoot(targetPath)) return result;
let elemUuid = rootUuid(targetPath);
// Walk up: collect elemUuid (from <dir>.xml of a sub-element) and the config root.
let cfgDir = null, binPath = null;
@@ -80,6 +101,8 @@ export function decideSupport(targetPath, require = 'editable') {
d = dirname(targetPath);
}
for (let i = 0; i < 12 && d; i++) {
// Crossed the boundary of an autonomous external object → allow (not on support).
if (isExternalObjectRoot(d + '.xml')) return result;
if (!elemUuid) elemUuid = rootUuid(d + '.xml');
if (!cfgDir) {
const cand = join(d, 'Ext', 'ParentConfigurations.bin');