mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-27 07:01:02 +03:00
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:
co-authored by
Claude Opus 4.8
parent
e7b5df4d50
commit
ddc1641176
@@ -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');
|
||||
|
||||
+40
-1
@@ -4,7 +4,7 @@
|
||||
// No live hook registration needed: exercises decideSupport / getEditMode / findConfigRoot
|
||||
// directly. Run: node hooks/test/run.mjs
|
||||
|
||||
import { decideSupport, findConfigRoot, rootUuid } from '../common/support-state.mjs';
|
||||
import { decideSupport, findConfigRoot, rootUuid, isExternalObjectRoot } from '../common/support-state.mjs';
|
||||
import { getEditMode, getSuggesterMode } from '../common/project.mjs';
|
||||
import { processInput as guard } from '../support-guard.mjs';
|
||||
import { processInput as suggest } from '../skill-suggester.mjs';
|
||||
@@ -96,6 +96,45 @@ console.log('=== support-state: synthetic per-object f1 (G=0) ===');
|
||||
check('synth remove removed (f1=2) → NOT blocked', rmRemoved.blocked === false, JSON.stringify(rmRemoved));
|
||||
}
|
||||
|
||||
console.log('=== support-state: external object boundary (issue #39) ===');
|
||||
{
|
||||
// Autonomous EPF/ERF parked INSIDE a config tree whose capability is off (G=1). The
|
||||
// climb must stop at the external object's root and NOT attribute it to the config.
|
||||
const ROOT = join(REPO, 'test-tmp', 'hooks-ext');
|
||||
const cat = '66666666-6666-6666-6666-666666666666';
|
||||
mkdirSync(join(ROOT, 'Ext'), { recursive: true });
|
||||
mkdirSync(join(ROOT, 'Catalogs'), { recursive: true });
|
||||
mkdirSync(join(ROOT, 'print-forms', 'Демо', 'Templates', 'Макет', 'Ext'), { recursive: true });
|
||||
// G=1 (capability off), K=1 — blocks every config object unconditionally.
|
||||
const binText = `{6,1,1,aaaaaaaa-0000-0000-0000-000000000000,0,bbbbbbbb-0000-0000-0000-000000000000,` +
|
||||
`"1.0","Vendor","Name",1,0,0,${cat}}`;
|
||||
writeFileSync(join(ROOT, 'Ext', 'ParentConfigurations.bin'),
|
||||
Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(binText, 'utf8')]));
|
||||
writeFileSync(join(ROOT, 'Configuration.xml'),
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<MetaDataObject><Configuration uuid="11111111-1111-1111-1111-111111111111"></Configuration></MetaDataObject>`);
|
||||
writeFileSync(join(ROOT, 'Catalogs', 'InConfig.xml'),
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<MetaDataObject><Catalog uuid="${cat}"></Catalog></MetaDataObject>`);
|
||||
writeFileSync(join(ROOT, 'print-forms', 'Демо.xml'),
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses">\n\t<ExternalDataProcessor uuid="99999999-9999-9999-9999-999999999999"><Properties><Name>Демо</Name></Properties></ExternalDataProcessor>\n</MetaDataObject>`);
|
||||
writeFileSync(join(ROOT, 'print-forms', 'Демо', 'Templates', 'Макет', 'Ext', 'Template.xml'),
|
||||
`<?xml version="1.0"?><document xmlns="http://v8.1c.ru/8.1/data/spreadsheet"></document>`);
|
||||
|
||||
const epfRoot = join(ROOT, 'print-forms', 'Демо.xml');
|
||||
const epfTpl = join(ROOT, 'print-forms', 'Демо', 'Templates', 'Макет', 'Ext', 'Template.xml');
|
||||
const cfgObj = join(ROOT, 'Catalogs', 'InConfig.xml');
|
||||
|
||||
check('isExternalObjectRoot(EPF root) → true', isExternalObjectRoot(epfRoot) === true);
|
||||
check('isExternalObjectRoot(config Catalog) → false', isExternalObjectRoot(cfgObj) === false);
|
||||
check('EPF root → NOT blocked (G=1 config ignored)', decideSupport(epfRoot, 'editable').blocked === false, JSON.stringify(decideSupport(epfRoot, 'editable')));
|
||||
const rTpl = decideSupport(epfTpl, 'editable');
|
||||
check('EPF template → NOT blocked (climb stops at EPF root)', rTpl.blocked === false, JSON.stringify(rTpl));
|
||||
const rCfg = decideSupport(cfgObj, 'editable');
|
||||
check('control: config object under G=1 → blocked', rCfg.blocked === true && rCfg.code === 'capability-off', JSON.stringify(rCfg));
|
||||
check('findConfigRoot(EPF template) → no cfgDir', findConfigRoot(epfTpl).cfgDir === null, JSON.stringify(findConfigRoot(epfTpl)));
|
||||
const rGuard = guard({ tool_name: 'Edit', cwd: REPO, tool_input: { file_path: epfTpl } });
|
||||
check('guard on EPF template → allow (no stdout)', rGuard.stdout === '' && rGuard.exitCode === 0, JSON.stringify(rGuard));
|
||||
}
|
||||
|
||||
console.log('=== project: reaction modes ===');
|
||||
{
|
||||
const m = getEditMode(ACC, REPO);
|
||||
|
||||
Reference in New Issue
Block a user