universal-dev-standards 6.10.0 → 6.11.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/bin/uds.js +2 -0
- package/bundled/locales/zh-CN/CHANGELOG.md +23 -3
- package/bundled/locales/zh-CN/README.md +1 -1
- package/bundled/locales/zh-CN/SECURITY.md +1 -1
- package/bundled/locales/zh-CN/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-CN/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-CN/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-TW/CHANGELOG.md +23 -3
- package/bundled/locales/zh-TW/README.md +1 -1
- package/bundled/locales/zh-TW/SECURITY.md +1 -1
- package/bundled/locales/zh-TW/docs/CHEATSHEET.md +1 -1
- package/bundled/locales/zh-TW/docs/CLI-INIT-OPTIONS.md +52 -5
- package/bundled/locales/zh-TW/docs/FEATURE-REFERENCE.md +3 -1
- package/bundled/locales/zh-TW/integrations/claude-code/README.md +14 -5
- package/package.json +1 -1
- package/src/commands/check.js +253 -21
- package/src/commands/config.js +15 -9
- package/src/commands/init.js +24 -3
- package/src/commands/update.js +311 -47
- package/src/core/manifest.js +39 -1
- package/src/flows/init-flow.js +9 -1
- package/src/generators/layered-claudemd.js +13 -4
- package/src/i18n/messages.js +3 -3
- package/src/installers/integration-installer.js +13 -6
- package/src/installers/manifest-installer.js +4 -0
- package/src/reconciler/actual-state-scanner.js +29 -2
- package/src/reconciler/desired-state-calculator.js +51 -2
- package/src/reconciler/diff-engine.js +19 -3
- package/src/reconciler/plan-executor.js +17 -16
- package/src/utils/hasher.js +61 -5
- package/src/utils/integration-generator.js +239 -28
- package/src/utils/marker-locator.js +140 -0
- package/src/utils/reference-sync.js +53 -1
- package/standards-registry.json +7 -7
|
@@ -3,8 +3,10 @@ import { dirname, join, basename } from 'path';
|
|
|
3
3
|
import { getLanguageRules } from '../prompts/integrations.js';
|
|
4
4
|
import { computeIntegrationBlockHash } from './hasher.js';
|
|
5
5
|
import { UDS_MARKERS, SUPPORTED_AI_TOOLS, LEGACY_TOOL_MAPPINGS } from '../core/constants.js';
|
|
6
|
+
import { locateMarkerBlock } from './marker-locator.js';
|
|
6
7
|
import { resolveSelectedOptionSources, resolveStandardFilename, getAllStandards } from './registry.js';
|
|
7
8
|
import { getAgentConfig, getAgentTier } from '../config/ai-agent-paths.js';
|
|
9
|
+
import { STANDARD_ID_MAPPING } from './conversion-rules.js';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Resolve contentMode and level based on AI tool's tier and capabilities.
|
|
@@ -2746,6 +2748,81 @@ function generateWorkflowGateContent(language) {
|
|
|
2746
2748
|
* @param {Object} config - Integration configuration
|
|
2747
2749
|
* @returns {string} Generated content
|
|
2748
2750
|
*/
|
|
2751
|
+
/**
|
|
2752
|
+
* Strip a standard's extension to get its bare identifying stem, e.g.
|
|
2753
|
+
* 'commit-message.ai.yaml' -> 'commit-message'. Shared by
|
|
2754
|
+
* resolveStandardReferences and getHeadingToPrimaryStandardStem below.
|
|
2755
|
+
*/
|
|
2756
|
+
function stemOf(value) {
|
|
2757
|
+
return basename(String(value)).replace(/\.(ai\.yaml|yaml|md)$/i, '');
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/**
|
|
2761
|
+
* Normalize a Markdown ATX heading line for comparison, e.g.
|
|
2762
|
+
* '## 提交訊息標準 ' -> '提交訊息標準'. Returns null for a non-heading line.
|
|
2763
|
+
*/
|
|
2764
|
+
function normalizeHeadingLine(line) {
|
|
2765
|
+
const m = String(line).match(/^#{1,6}\s+(.+?)\s*$/);
|
|
2766
|
+
return m ? m[1].trim() : null;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
let _headingToPrimaryStandardStem = null;
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Map every RULE_TEMPLATES section heading (any category, detail level,
|
|
2773
|
+
* language that has a Reference:/參考: line) to the stem of the FIRST
|
|
2774
|
+
* `.standards/...` path on that line — that section's "primary standard".
|
|
2775
|
+
*
|
|
2776
|
+
* XSPEC adopter-report, narrow auto-repair (decision 1, 2026-09-18):
|
|
2777
|
+
* 6.10.0's resolveStandardReferences deleted `.standards/commit-message-guide.md`
|
|
2778
|
+
* from an existing file's "## 提交訊息標準" section — it looked exactly like a
|
|
2779
|
+
* retired, uninstalled UDS standard — leaving only the trailing options
|
|
2780
|
+
* reference behind. Rewriting away the resulting dangling comma (the earlier
|
|
2781
|
+
* fix, same XSPEC) does not bring the deleted item back: it is gone from the
|
|
2782
|
+
* file, and nothing else regenerates that section, because rule-template
|
|
2783
|
+
* sections live OUTSIDE the UDS markers by design.
|
|
2784
|
+
*
|
|
2785
|
+
* This map is how resolveStandardReferences recognizes "this heading belongs
|
|
2786
|
+
* to a UDS rule template, and here is the one reference it must never be
|
|
2787
|
+
* missing" — so it can restore ONLY that single item, ONLY when it is
|
|
2788
|
+
* genuinely installed and missing from the line. A heading that does not
|
|
2789
|
+
* match any template (a user's own section) is simply absent from this map,
|
|
2790
|
+
* so it is never touched.
|
|
2791
|
+
*
|
|
2792
|
+
* Computed once and cached — RULE_TEMPLATES is a static module constant.
|
|
2793
|
+
*
|
|
2794
|
+
* @returns {Map<string, string>} heading text (without leading '#'s) -> primary standard stem
|
|
2795
|
+
*/
|
|
2796
|
+
function getHeadingToPrimaryStandardStem() {
|
|
2797
|
+
if (_headingToPrimaryStandardStem) return _headingToPrimaryStandardStem;
|
|
2798
|
+
|
|
2799
|
+
const map = new Map();
|
|
2800
|
+
const refLineRe = /^(?:Reference|參考|参考)[::]([^\n]*)$/im;
|
|
2801
|
+
|
|
2802
|
+
for (const categoryTemplates of Object.values(RULE_TEMPLATES)) {
|
|
2803
|
+
for (const modeTemplates of Object.values(categoryTemplates)) {
|
|
2804
|
+
for (const text of Object.values(modeTemplates)) {
|
|
2805
|
+
const heading = normalizeHeadingLine(String(text).split('\n')[0]);
|
|
2806
|
+
if (!heading) continue;
|
|
2807
|
+
|
|
2808
|
+
// 'minimal' mode has no Reference: line at all — nothing to repair,
|
|
2809
|
+
// and no primary standard to record for that heading.
|
|
2810
|
+
const refMatch = text.match(refLineRe);
|
|
2811
|
+
if (!refMatch) continue;
|
|
2812
|
+
|
|
2813
|
+
const firstItem = refMatch[1].split(',')[0].trim();
|
|
2814
|
+
const pathMatch = firstItem.match(/^\.standards\/([^\s,`)\]]+)$/);
|
|
2815
|
+
if (!pathMatch) continue;
|
|
2816
|
+
|
|
2817
|
+
map.set(heading, stemOf(pathMatch[1]));
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
_headingToPrimaryStandardStem = map;
|
|
2823
|
+
return map;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2749
2826
|
/**
|
|
2750
2827
|
* Point every `.standards/<file>` reference at the file this project actually has.
|
|
2751
2828
|
*
|
|
@@ -2769,7 +2846,6 @@ function generateWorkflowGateContent(language) {
|
|
|
2769
2846
|
function resolveStandardReferences(content, installedStandards = [], standardsFormat = 'ai', projectPath = null) {
|
|
2770
2847
|
if (!content) return content;
|
|
2771
2848
|
|
|
2772
|
-
const stemOf = (value) => basename(String(value)).replace(/\.(ai\.yaml|yaml|md)$/i, '');
|
|
2773
2849
|
// Every stem UDS itself ships — the only references this function may delete.
|
|
2774
2850
|
//
|
|
2775
2851
|
// Read defensively: this runs inside pure content generation, and callers
|
|
@@ -2796,15 +2872,68 @@ function resolveStandardReferences(content, installedStandards = [], standardsFo
|
|
|
2796
2872
|
installedByStem.set(stemOf(filename), filename);
|
|
2797
2873
|
}
|
|
2798
2874
|
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2875
|
+
// XSPEC adopter-report Q2: a reference written against a pre-6.0.0
|
|
2876
|
+
// filename (`commit-message-guide.md`) must still resolve to whatever this
|
|
2877
|
+
// project installed under the CURRENT id (`commit-message`) — that rename
|
|
2878
|
+
// is exactly what STANDARD_ID_MAPPING already records, previously
|
|
2879
|
+
// consulted only by the human→AI YAML generator. Without it, the old name
|
|
2880
|
+
// was indistinguishable from a retired, UDS-owned standard the project
|
|
2881
|
+
// chose not to install, and got deleted instead of rewritten.
|
|
2882
|
+
const resolveActualFilename = (path) => {
|
|
2883
|
+
const stem = stemOf(path);
|
|
2884
|
+
return installedByStem.get(stem) || installedByStem.get(STANDARD_ID_MAPPING[stem] || stem) || null;
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
const headingToPrimaryStem = getHeadingToPrimaryStandardStem();
|
|
2888
|
+
const headingLineRe = /^#{1,6}\s+/;
|
|
2889
|
+
const referenceLineRe = /^([^\n]*(?:Reference|參考|参考)[::])([^\n]*)$/i;
|
|
2890
|
+
|
|
2891
|
+
// XSPEC adopter-report Q2: rebuilt as a line-by-line split → filter → join
|
|
2892
|
+
// instead of a single global regex, so each "Reference:" line's PRECEDING
|
|
2893
|
+
// heading is known (needed for decision 1's narrow auto-repair below) —
|
|
2894
|
+
// and instead of patching a deleted substring's separators with more
|
|
2895
|
+
// regexes. The old single-regex approach deleted only the matched
|
|
2896
|
+
// `.standards/...` text in place and then tried to tidy up whatever
|
|
2897
|
+
// punctuation was left around the hole; it handled a trailing comma, a
|
|
2898
|
+
// doubled comma, and a comma with stray space, but not a comma left
|
|
2899
|
+
// dangling right after the colon when the FIRST item was the one dropped
|
|
2900
|
+
// (`Reference:, .standards/other.md`).
|
|
2901
|
+
const lines = content.split('\n');
|
|
2902
|
+
let currentHeading = null;
|
|
2903
|
+
const outLines = [];
|
|
2904
|
+
|
|
2905
|
+
for (const rawLine of lines) {
|
|
2906
|
+
if (headingLineRe.test(rawLine)) {
|
|
2907
|
+
currentHeading = normalizeHeadingLine(rawLine);
|
|
2908
|
+
outLines.push(rawLine);
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
const refMatch = rawLine.match(referenceLineRe);
|
|
2913
|
+
if (!refMatch) {
|
|
2914
|
+
outLines.push(rawLine);
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const [, label, rest] = refMatch;
|
|
2919
|
+
const items = rest.split(',').map((s) => s.trim()).filter(Boolean);
|
|
2920
|
+
const kept = [];
|
|
2921
|
+
const presentStems = new Set();
|
|
2922
|
+
|
|
2923
|
+
for (const item of items) {
|
|
2924
|
+
const pathMatch = item.match(/^(\.standards\/)([^\s,`)\]]+)$/);
|
|
2925
|
+
if (!pathMatch) {
|
|
2926
|
+
// Not a bare `.standards/...` reference (e.g. trailing prose) — leave untouched.
|
|
2927
|
+
kept.push(item);
|
|
2928
|
+
continue;
|
|
2929
|
+
}
|
|
2930
|
+
const [, prefix, path] = pathMatch;
|
|
2931
|
+
|
|
2804
2932
|
// Option files live in `.standards/options/…` and are named by path, not ID.
|
|
2805
|
-
if (path.startsWith('options/')) { kept
|
|
2806
|
-
|
|
2807
|
-
|
|
2933
|
+
if (path.startsWith('options/')) { kept.push(item); presentStems.add(stemOf(path)); continue; }
|
|
2934
|
+
|
|
2935
|
+
const actual = resolveActualFilename(path);
|
|
2936
|
+
if (actual) { kept.push(`${prefix}${actual}`); presentStems.add(stemOf(actual)); continue; }
|
|
2808
2937
|
|
|
2809
2938
|
// 🔴 Only UDS's own standards may be dropped. The first version of this
|
|
2810
2939
|
// rewrite removed every unmatched reference, which deleted a project's own
|
|
@@ -2816,14 +2945,38 @@ function resolveStandardReferences(content, installedStandards = [], standardsFo
|
|
|
2816
2945
|
} catch {
|
|
2817
2946
|
onDisk = false;
|
|
2818
2947
|
}
|
|
2819
|
-
if (onDisk || !udsOwnedStems.has(stemOf(path))) {
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2948
|
+
if (onDisk || !udsOwnedStems.has(stemOf(path))) {
|
|
2949
|
+
kept.push(item);
|
|
2950
|
+
presentStems.add(stemOf(path));
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
// Dropped: a UDS-owned reference to a standard this project did not install.
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// XSPEC adopter-report, narrow auto-repair (decision 1): this line's
|
|
2957
|
+
// heading matches a UDS rule template EXACTLY (see
|
|
2958
|
+
// getHeadingToPrimaryStandardStem's docblock) and that template's
|
|
2959
|
+
// primary standard is genuinely installed but missing from the line —
|
|
2960
|
+
// put it back at the front. Idempotent: a line that already has it does
|
|
2961
|
+
// nothing. Everything else about the line — the project's own entries,
|
|
2962
|
+
// options files, existing order — is untouched, and no OTHER (secondary)
|
|
2963
|
+
// template item is ever added back.
|
|
2964
|
+
const primaryStem = currentHeading ? headingToPrimaryStem.get(currentHeading) : null;
|
|
2965
|
+
if (primaryStem && !presentStems.has(primaryStem)) {
|
|
2966
|
+
const primaryActual = installedByStem.get(primaryStem);
|
|
2967
|
+
if (primaryActual) {
|
|
2968
|
+
kept.unshift(`.standards/${primaryActual}`);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
if (kept.length === 0) {
|
|
2973
|
+
outLines.push('');
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
outLines.push(`${label} ${kept.join(', ')}`);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
return outLines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
2827
2980
|
}
|
|
2828
2981
|
|
|
2829
2982
|
export function generateIntegrationContent(config) {
|
|
@@ -3030,7 +3183,10 @@ export function mergeRules(existingContent, newContent, strategy) {
|
|
|
3030
3183
|
* @returns {Object} Result with success status
|
|
3031
3184
|
*/
|
|
3032
3185
|
export function writeIntegrationFile(tool, config, projectPath) {
|
|
3033
|
-
|
|
3186
|
+
// XSPEC-418 R2/R3: config.integrationTargets carries manifest.integrationTargets
|
|
3187
|
+
// (or its init-time equivalent) through unchanged — this is the actual write path,
|
|
3188
|
+
// so it must be the one place that honors a per-tool target override.
|
|
3189
|
+
const fileName = resolveIntegrationTargetFile(tool, { integrationTargets: config.integrationTargets });
|
|
3034
3190
|
if (!fileName) {
|
|
3035
3191
|
return { success: false, error: `Unknown tool: ${tool}` };
|
|
3036
3192
|
}
|
|
@@ -3097,10 +3253,13 @@ export function writeIntegrationFile(tool, config, projectPath) {
|
|
|
3097
3253
|
* Check if integration file exists
|
|
3098
3254
|
* @param {string} tool - Tool name
|
|
3099
3255
|
* @param {string} projectPath - Project root path
|
|
3256
|
+
* @param {Object} [manifestLike] - Manifest-like object; only `integrationTargets` is
|
|
3257
|
+
* read (XSPEC-418 R2) — pass the real manifest, or `{ integrationTargets }` at
|
|
3258
|
+
* init time before a manifest object exists.
|
|
3100
3259
|
* @returns {boolean} True if file exists
|
|
3101
3260
|
*/
|
|
3102
|
-
export function integrationFileExists(tool, projectPath) {
|
|
3103
|
-
const fileName =
|
|
3261
|
+
export function integrationFileExists(tool, projectPath, manifestLike) {
|
|
3262
|
+
const fileName = resolveIntegrationTargetFile(tool, manifestLike);
|
|
3104
3263
|
return fileName && existsSync(join(projectPath, fileName));
|
|
3105
3264
|
}
|
|
3106
3265
|
|
|
@@ -3334,7 +3493,12 @@ export function buildToolIntegrationConfig(manifest, tool) {
|
|
|
3334
3493
|
contentMode: resolved.contentMode,
|
|
3335
3494
|
level: resolved.level,
|
|
3336
3495
|
outputLanguage: selected,
|
|
3337
|
-
methodology: manifest.methodology
|
|
3496
|
+
methodology: manifest.methodology,
|
|
3497
|
+
// XSPEC-418 R2/R3: threads the per-tool target override through to
|
|
3498
|
+
// writeIntegrationFile via resolveIntegrationTargetFile. Every caller of
|
|
3499
|
+
// this function (regenerateIntegrations, plan-executor's migrate_block,
|
|
3500
|
+
// backfillIntegrationConfigs) gets the override for free.
|
|
3501
|
+
integrationTargets: manifest.integrationTargets
|
|
3338
3502
|
};
|
|
3339
3503
|
}
|
|
3340
3504
|
|
|
@@ -3394,13 +3558,18 @@ export function wrapWithMarkers(content, format) {
|
|
|
3394
3558
|
*/
|
|
3395
3559
|
export function extractMarkedContent(fileContent, format) {
|
|
3396
3560
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3561
|
+
// XSPEC adopter-report Q5: locateMarkerBlock requires the marker to occupy
|
|
3562
|
+
// a whole line by itself (and not be inside a fenced code block), so a
|
|
3563
|
+
// sentence that merely mentions the marker text is never mistaken for the
|
|
3564
|
+
// real boundary. It throws AmbiguousMarkerError if more than one real pair
|
|
3565
|
+
// exists — callers must not catch that away silently.
|
|
3566
|
+
const block = locateMarkerBlock(fileContent, markers);
|
|
3567
|
+
|
|
3568
|
+
if (!block) {
|
|
3401
3569
|
return { before: fileContent, content: '', after: '' };
|
|
3402
3570
|
}
|
|
3403
3571
|
|
|
3572
|
+
const { startIdx, endIdx } = block;
|
|
3404
3573
|
return {
|
|
3405
3574
|
before: fileContent.substring(0, startIdx),
|
|
3406
3575
|
content: fileContent.substring(startIdx + markers.start.length, endIdx).trim(),
|
|
@@ -3417,15 +3586,18 @@ export function extractMarkedContent(fileContent, format) {
|
|
|
3417
3586
|
*/
|
|
3418
3587
|
export function updateMarkedSection(existingContent, newMarkedContent, format) {
|
|
3419
3588
|
const markers = UDS_MARKERS[format] || UDS_MARKERS.markdown;
|
|
3420
|
-
|
|
3421
|
-
|
|
3589
|
+
// XSPEC adopter-report Q5: same rule as extractMarkedContent — a line that
|
|
3590
|
+
// merely mentions the marker text is not a boundary, so it is never
|
|
3591
|
+
// deleted along with (what used to be mistaken for) "the UDS block".
|
|
3592
|
+
const block = locateMarkerBlock(existingContent, markers);
|
|
3422
3593
|
|
|
3423
|
-
if (
|
|
3594
|
+
if (!block) {
|
|
3424
3595
|
// No existing markers, append new content
|
|
3425
3596
|
return existingContent.trim() + '\n\n' + wrapWithMarkers(newMarkedContent, format) + '\n';
|
|
3426
3597
|
}
|
|
3427
3598
|
|
|
3428
3599
|
// Replace existing marked section
|
|
3600
|
+
const { startIdx, endIdx } = block;
|
|
3429
3601
|
const before = existingContent.substring(0, startIdx);
|
|
3430
3602
|
const after = existingContent.substring(endIdx + markers.end.length);
|
|
3431
3603
|
|
|
@@ -3441,6 +3613,45 @@ export function getToolFilePath(tool) {
|
|
|
3441
3613
|
return getToolFileName(tool) || null;
|
|
3442
3614
|
}
|
|
3443
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Tool × manifest → the integration file this tool actually reads/writes.
|
|
3618
|
+
*
|
|
3619
|
+
* XSPEC-418 R2: honors a per-tool override recorded in
|
|
3620
|
+
* `manifest.integrationTargets` (currently only ever set for `claude-code`, to
|
|
3621
|
+
* `CLAUDE.local.md` — a personal UDS adoption in a repo with a team-owned,
|
|
3622
|
+
* version-controlled `CLAUDE.md`). No override set → same file
|
|
3623
|
+
* `getToolFilePath` already returns, so every manifest that never sets
|
|
3624
|
+
* `integrationTargets` gets byte-identical output (XSPEC-418 AC-5).
|
|
3625
|
+
*
|
|
3626
|
+
* This is the single place "tool → target file" is decided once a manifest (or
|
|
3627
|
+
* a manifest-shaped config carrying `integrationTargets`) is in scope. Every
|
|
3628
|
+
* call site that used to call `getToolFilePath`/`getToolFileName` to answer
|
|
3629
|
+
* "what file do I read/write for this tool right now" must call this instead
|
|
3630
|
+
* — enforced by the static-scan guard in
|
|
3631
|
+
* tests/unit/core/integration-target-resolver-guard.test.js.
|
|
3632
|
+
*
|
|
3633
|
+
* Placed here rather than core/constants.js, where XSPEC-418 originally
|
|
3634
|
+
* suggested it next to `resolveIntegrationFile`: the default-file fallback
|
|
3635
|
+
* must be the `getToolFileName` above — the one with the legacy-mapping guard,
|
|
3636
|
+
* the already-a-filename passthrough, and the "known agent missing from
|
|
3637
|
+
* SUPPORTED_AI_TOOLS" throw (see its own comment) — not a re-implementation in
|
|
3638
|
+
* constants.js, which would silently drop that throw for every call site this
|
|
3639
|
+
* migrates. constants.js is imported BY this file already, so this file
|
|
3640
|
+
* cannot be imported back from constants.js without a cycle; it lives beside
|
|
3641
|
+
* the function it wraps instead.
|
|
3642
|
+
*
|
|
3643
|
+
* @param {string} tool - Tool key (e.g. 'claude-code'), or an entry already
|
|
3644
|
+
* resolved to a file path — passed straight to `getToolFileName`, which
|
|
3645
|
+
* already tolerates that shape (XSPEC-208 BUG-208-01).
|
|
3646
|
+
* @param {Object} [manifest] - Manifest-like object; only `integrationTargets` is read.
|
|
3647
|
+
* @returns {string|null} Repo-relative target file, or null for a genuinely unknown tool.
|
|
3648
|
+
*/
|
|
3649
|
+
export function resolveIntegrationTargetFile(tool, manifest) {
|
|
3650
|
+
const key = LEGACY_TOOL_MAPPINGS[tool] || tool;
|
|
3651
|
+
const override = manifest?.integrationTargets?.[key];
|
|
3652
|
+
if (typeof override === 'string' && override.length > 0) return override;
|
|
3653
|
+
return getToolFileName(tool) || null;
|
|
3654
|
+
}
|
|
3444
3655
|
|
|
3445
3656
|
/**
|
|
3446
3657
|
* Get default commands based on ecosystem
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared marker-block location logic for UDS-managed sections in integration
|
|
3
|
+
* files (CLAUDE.md, AGENTS.md, etc.).
|
|
4
|
+
*
|
|
5
|
+
* XSPEC adopter-report Q5 (v3.5.0–6.10.0): every call site that located the
|
|
6
|
+
* `<!-- UDS:STANDARDS:START -->` / `<!-- UDS:STANDARDS:END -->` pair used
|
|
7
|
+
* `content.indexOf(markers.start)` / `.indexOf(markers.end)` directly. That
|
|
8
|
+
* matches the marker text anywhere in the file — including inside a sentence
|
|
9
|
+
* that merely *mentions* it (this project's own CLAUDE.md explains the marker
|
|
10
|
+
* syntax in prose, `<!-- UDS:STANDARDS:START -->`, inline) or inside a fenced
|
|
11
|
+
* code sample quoting it. A write path built on that wrong index then treated
|
|
12
|
+
* everything between the mention and the real END marker as "the UDS block"
|
|
13
|
+
* and deleted it — including the user's own content in between.
|
|
14
|
+
*
|
|
15
|
+
* The fix: a marker only counts when, after trimming, it occupies an entire
|
|
16
|
+
* line by itself, and that line is not inside a fenced code block (``` or
|
|
17
|
+
* ~~~). Every reader of a marker pair in this codebase must go through
|
|
18
|
+
* `locateMarkerBlock`/`tryLocateMarkerBlock` instead of indexOf/includes —
|
|
19
|
+
* enforced by a static-scan guard test.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} MarkerLine
|
|
24
|
+
* @property {number} offset - Character offset of the marker text itself
|
|
25
|
+
* within `content` (not the start of the line — leading whitespace before
|
|
26
|
+
* the marker, if any, is excluded).
|
|
27
|
+
* @property {number} line - 1-based line number.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find every standalone-line, non-fenced-code occurrence of `markerText`.
|
|
32
|
+
* @param {string} content
|
|
33
|
+
* @param {string} markerText
|
|
34
|
+
* @returns {MarkerLine[]}
|
|
35
|
+
*/
|
|
36
|
+
function findMarkerLines(content, markerText) {
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
const occurrences = [];
|
|
39
|
+
let offset = 0;
|
|
40
|
+
let inFence = false;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const rawLine = lines[i];
|
|
44
|
+
const trimmed = rawLine.trim();
|
|
45
|
+
|
|
46
|
+
if (/^(`{3,}|~{3,})/.test(trimmed)) {
|
|
47
|
+
// Fenced code block delimiter (``` or ```lang, closing ``` too) — a
|
|
48
|
+
// marker string appearing between a pair of these is a documentation
|
|
49
|
+
// sample, not a real boundary. This intentionally does not try to
|
|
50
|
+
// distinguish open/close by matching fence characters or length; any
|
|
51
|
+
// triple-backtick-or-tilde line toggles the state, matching how
|
|
52
|
+
// Markdown renderers treat fences in practice.
|
|
53
|
+
inFence = !inFence;
|
|
54
|
+
} else if (!inFence && trimmed === markerText) {
|
|
55
|
+
const leading = rawLine.length - rawLine.trimStart().length;
|
|
56
|
+
occurrences.push({ offset: offset + leading, line: i + 1 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
offset += rawLine.length + 1; // +1 for the '\n' that split('\n') consumed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return occurrences;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Thrown when a marker's start or end text has more than one standalone,
|
|
67
|
+
* non-fenced occurrence in a file — the file is ambiguous and no caller
|
|
68
|
+
* should guess which one is the real boundary.
|
|
69
|
+
*/
|
|
70
|
+
export class AmbiguousMarkerError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} markerText
|
|
73
|
+
* @param {number[]} lines - 1-based line numbers of every occurrence found.
|
|
74
|
+
*/
|
|
75
|
+
constructor(markerText, lines) {
|
|
76
|
+
super(
|
|
77
|
+
`Found ${lines.length} standalone occurrences of marker "${markerText}" ` +
|
|
78
|
+
`on line(s) ${lines.join(', ')} — expected exactly one. Refusing to guess ` +
|
|
79
|
+
'which one bounds the UDS-managed block.'
|
|
80
|
+
);
|
|
81
|
+
this.name = 'AmbiguousMarkerError';
|
|
82
|
+
this.markerText = markerText;
|
|
83
|
+
this.lines = lines;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Locate the single UDS marker block in `content`.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} content
|
|
91
|
+
* @param {{start: string, end: string}} markers
|
|
92
|
+
* @returns {{startIdx: number, endIdx: number, startLine: number, endLine: number} | null}
|
|
93
|
+
* `null` means "this file has no UDS block" — the same meaning the old
|
|
94
|
+
* `startIdx === -1` check carried. This is not an error.
|
|
95
|
+
* @throws {AmbiguousMarkerError} when more than one valid START or END line
|
|
96
|
+
* exists — the file has two or more marker pairs, or a corrupted one.
|
|
97
|
+
*/
|
|
98
|
+
export function locateMarkerBlock(content, markers) {
|
|
99
|
+
const starts = findMarkerLines(content, markers.start);
|
|
100
|
+
const ends = findMarkerLines(content, markers.end);
|
|
101
|
+
|
|
102
|
+
if (starts.length > 1) {
|
|
103
|
+
throw new AmbiguousMarkerError(markers.start, starts.map((s) => s.line));
|
|
104
|
+
}
|
|
105
|
+
if (ends.length > 1) {
|
|
106
|
+
throw new AmbiguousMarkerError(markers.end, ends.map((e) => e.line));
|
|
107
|
+
}
|
|
108
|
+
if (starts.length === 0 || ends.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const startIdx = starts[0].offset;
|
|
113
|
+
const endIdx = ends[0].offset;
|
|
114
|
+
if (endIdx <= startIdx) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { startIdx, endIdx, startLine: starts[0].line, endLine: ends[0].line };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Non-throwing variant of `locateMarkerBlock` for callers that must not
|
|
123
|
+
* crash a broader scan on one ambiguous file (e.g. a full-project state
|
|
124
|
+
* scan reconciling many files at once). Ambiguity is reported through the
|
|
125
|
+
* return value instead of an exception.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} content
|
|
128
|
+
* @param {{start: string, end: string}} markers
|
|
129
|
+
* @returns {{ok: true, block: ReturnType<typeof locateMarkerBlock>} | {ok: false, error: AmbiguousMarkerError}}
|
|
130
|
+
*/
|
|
131
|
+
export function tryLocateMarkerBlock(content, markers) {
|
|
132
|
+
try {
|
|
133
|
+
return { ok: true, block: locateMarkerBlock(content, markers) };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof AmbiguousMarkerError) {
|
|
136
|
+
return { ok: false, error };
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -338,7 +338,12 @@ export function calculateCategoriesFromStandards(standards) {
|
|
|
338
338
|
const categories = new Set();
|
|
339
339
|
|
|
340
340
|
for (const std of standards) {
|
|
341
|
-
|
|
341
|
+
// XSPEC adopter-report Q1: getStandardCategory is keyed by filename and
|
|
342
|
+
// returns null for a bare manifest stem ('commit-message'), which is
|
|
343
|
+
// exactly what a 3.4.0 manifest stores — this emptied the whole category
|
|
344
|
+
// set on a real install. categoryForStandard (below) tries the stem
|
|
345
|
+
// against both known extensions first.
|
|
346
|
+
const category = categoryForStandard(std);
|
|
342
347
|
if (category) {
|
|
343
348
|
categories.add(category);
|
|
344
349
|
}
|
|
@@ -347,6 +352,53 @@ export function calculateCategoriesFromStandards(standards) {
|
|
|
347
352
|
return Array.from(categories);
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Whether a stored `categories` array is broken and should not be trusted:
|
|
357
|
+
* missing/not-an-array, empty, or containing a value this build does not
|
|
358
|
+
* recognize as a category id.
|
|
359
|
+
*
|
|
360
|
+
* @param {*} categories - `manifest.integrationConfigs[file].categories`
|
|
361
|
+
* @returns {boolean}
|
|
362
|
+
*/
|
|
363
|
+
function categoriesLookBroken(categories) {
|
|
364
|
+
if (!Array.isArray(categories) || categories.length === 0) return true;
|
|
365
|
+
const known = new Set(Object.keys(CATEGORY_TO_STANDARDS));
|
|
366
|
+
return categories.some((c) => !known.has(c));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Repair `manifest.integrationConfigs[file].categories` wherever it is empty
|
|
371
|
+
* or contains an unrecognized value, recomputing it from `manifest.standards`
|
|
372
|
+
* the same way `calculateCategoriesFromStandards` (used by `--sync-refs`)
|
|
373
|
+
* does.
|
|
374
|
+
*
|
|
375
|
+
* XSPEC adopter-report Q1 follow-up (main-session review): the original Q1
|
|
376
|
+
* fix repaired this only inside `--sync-refs`. A manifest that picked up a
|
|
377
|
+
* broken `categories: []` from an older buggy `--sync-refs` run stayed
|
|
378
|
+
* broken forever afterward — a plain `uds update` reaches its OWN,
|
|
379
|
+
* independent integration-sync code path (update.js's general update flow)
|
|
380
|
+
* which writes `integrationBlockHashes` but never even reads
|
|
381
|
+
* `integrationConfigs`, so nothing there ever corrected it. The next `uds
|
|
382
|
+
* check --restore-missing` then rebuilt the file from that broken stored
|
|
383
|
+
* config and silently dropped sections. Deliberately conservative: an
|
|
384
|
+
* already-valid (if outdated) categories list is left alone — that is
|
|
385
|
+
* `--sync-refs`'s job, not every plain `update`'s — this only self-heals
|
|
386
|
+
* outright corruption (empty or unrecognized values).
|
|
387
|
+
*
|
|
388
|
+
* @param {Object} manifest - Manifest object (mutated in place)
|
|
389
|
+
* @returns {string[]} Paths whose `categories` were repaired
|
|
390
|
+
*/
|
|
391
|
+
export function repairIntegrationConfigCategories(manifest) {
|
|
392
|
+
if (!manifest?.integrationConfigs) return [];
|
|
393
|
+
const repaired = [];
|
|
394
|
+
for (const [file, config] of Object.entries(manifest.integrationConfigs)) {
|
|
395
|
+
if (!config || !categoriesLookBroken(config.categories)) continue;
|
|
396
|
+
config.categories = calculateCategoriesFromStandards(manifest.standards || []);
|
|
397
|
+
repaired.push(file);
|
|
398
|
+
}
|
|
399
|
+
return repaired;
|
|
400
|
+
}
|
|
401
|
+
|
|
350
402
|
/**
|
|
351
403
|
* Get all standard filenames that should be referenced for given categories
|
|
352
404
|
*
|
package/standards-registry.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
4
4
|
"lastUpdated": "2026-05-13",
|
|
5
5
|
"description": "Standards registry for universal-dev-standards with integrated skills and AI-optimized formats",
|
|
6
6
|
"formats": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"standards": {
|
|
59
59
|
"name": "universal-dev-standards",
|
|
60
60
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
61
|
-
"version": "6.
|
|
61
|
+
"version": "6.11.0"
|
|
62
62
|
},
|
|
63
63
|
"skills": {
|
|
64
64
|
"name": "universal-dev-standards",
|
|
65
65
|
"url": "https://github.com/AsiaOstrich/universal-dev-standards",
|
|
66
66
|
"localPath": "skills",
|
|
67
67
|
"rawUrl": "https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills",
|
|
68
|
-
"version": "6.
|
|
68
|
+
"version": "6.11.0",
|
|
69
69
|
"note": "Skills are now included in the main repository under skills/"
|
|
70
70
|
}
|
|
71
71
|
},
|
|
@@ -2283,7 +2283,7 @@
|
|
|
2283
2283
|
"id": "license-compliance",
|
|
2284
2284
|
"name": "License Compliance Standards",
|
|
2285
2285
|
"nameZh": "授權合規標準",
|
|
2286
|
-
"version": "6.
|
|
2286
|
+
"version": "6.11.0",
|
|
2287
2287
|
"source": {
|
|
2288
2288
|
"human": "core/license-compliance.md",
|
|
2289
2289
|
"ai": "ai/standards/license-compliance.ai.yaml"
|
|
@@ -2295,7 +2295,7 @@
|
|
|
2295
2295
|
"id": "verification-oracle",
|
|
2296
2296
|
"name": "Verification Oracle Standards",
|
|
2297
2297
|
"nameZh": "驗證 Oracle 標準",
|
|
2298
|
-
"version": "6.
|
|
2298
|
+
"version": "6.11.0",
|
|
2299
2299
|
"source": {
|
|
2300
2300
|
"human": "core/verification-oracle.md",
|
|
2301
2301
|
"ai": "ai/standards/verification-oracle.ai.yaml"
|
|
@@ -2307,7 +2307,7 @@
|
|
|
2307
2307
|
"id": "model-provenance",
|
|
2308
2308
|
"name": "Model Provenance Policy Standards",
|
|
2309
2309
|
"nameZh": "模型來源政策標準",
|
|
2310
|
-
"version": "6.
|
|
2310
|
+
"version": "6.11.0",
|
|
2311
2311
|
"source": {
|
|
2312
2312
|
"human": "core/model-provenance.md",
|
|
2313
2313
|
"ai": "ai/standards/model-provenance.ai.yaml"
|
|
@@ -2319,7 +2319,7 @@
|
|
|
2319
2319
|
"id": "resource-cost-boundary",
|
|
2320
2320
|
"name": "Resource / Cost Boundary Declaration Standards",
|
|
2321
2321
|
"nameZh": "資源/成本邊界宣告標準",
|
|
2322
|
-
"version": "6.
|
|
2322
|
+
"version": "6.11.0",
|
|
2323
2323
|
"source": {
|
|
2324
2324
|
"human": "core/resource-cost-boundary.md",
|
|
2325
2325
|
"ai": "ai/standards/resource-cost-boundary.ai.yaml"
|