universal-dev-standards 6.9.0 → 6.10.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.
Files changed (47) hide show
  1. package/bundled/core/agent-communication-protocol.md +8 -0
  2. package/bundled/core/branch-completion.md +8 -0
  3. package/bundled/core/change-batching-standards.md +8 -0
  4. package/bundled/core/execution-history.md +8 -0
  5. package/bundled/core/pipeline-integration-standards.md +8 -0
  6. package/bundled/core/workflow-enforcement.md +8 -0
  7. package/bundled/core/workflow-state-protocol.md +8 -0
  8. package/bundled/locales/zh-CN/CHANGELOG.md +29 -3
  9. package/bundled/locales/zh-CN/README.md +1 -1
  10. package/bundled/locales/zh-CN/SECURITY.md +1 -1
  11. package/bundled/locales/zh-CN/core/agent-communication-protocol.md +7 -0
  12. package/bundled/locales/zh-CN/core/branch-completion.md +7 -0
  13. package/bundled/locales/zh-CN/core/change-batching-standards.md +7 -0
  14. package/bundled/locales/zh-CN/core/execution-history.md +7 -0
  15. package/bundled/locales/zh-CN/core/pipeline-integration-standards.md +7 -0
  16. package/bundled/locales/zh-CN/core/workflow-enforcement.md +7 -0
  17. package/bundled/locales/zh-CN/core/workflow-state-protocol.md +7 -0
  18. package/bundled/locales/zh-CN/docs/MIGRATION-v6.md +8 -4
  19. package/bundled/locales/zh-TW/CHANGELOG.md +30 -3
  20. package/bundled/locales/zh-TW/README.md +1 -1
  21. package/bundled/locales/zh-TW/SECURITY.md +1 -1
  22. package/bundled/locales/zh-TW/core/agent-communication-protocol.md +7 -0
  23. package/bundled/locales/zh-TW/core/branch-completion.md +7 -0
  24. package/bundled/locales/zh-TW/core/change-batching-standards.md +7 -0
  25. package/bundled/locales/zh-TW/core/execution-history.md +7 -0
  26. package/bundled/locales/zh-TW/core/pipeline-integration-standards.md +7 -0
  27. package/bundled/locales/zh-TW/core/workflow-enforcement.md +7 -0
  28. package/bundled/locales/zh-TW/core/workflow-state-protocol.md +7 -0
  29. package/bundled/locales/zh-TW/docs/MIGRATION-v6.md +8 -4
  30. package/package.json +7 -6
  31. package/src/commands/check.js +161 -24
  32. package/src/commands/config.js +19 -19
  33. package/src/commands/init.js +13 -4
  34. package/src/commands/spec.js +2 -2
  35. package/src/commands/update.js +251 -29
  36. package/src/i18n/messages.js +36 -0
  37. package/src/installers/integration-installer.js +4 -4
  38. package/src/installers/skills-installer.js +4 -4
  39. package/src/installers/standards-installer.js +3 -3
  40. package/src/prompts/init.js +33 -4
  41. package/src/reconciler/diff-engine.js +57 -2
  42. package/src/reconciler/plan-executor.js +31 -11
  43. package/src/utils/integration-generator.js +209 -81
  44. package/src/utils/reference-sync.js +103 -7
  45. package/src/utils/registry.js +57 -0
  46. package/src/utils/spinner.js +31 -0
  47. package/standards-registry.json +8 -8
@@ -1,3 +1,6 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
1
4
  /**
2
5
  * Reference Sync Utilities
3
6
  *
@@ -109,6 +112,56 @@ export function parseReferences(content) {
109
112
  return Array.from(references);
110
113
  }
111
114
 
115
+ /**
116
+ * Every `.standards/…` or `core/…` path mentioned anywhere in the file that is
117
+ * not on disk.
118
+ *
119
+ * `parseReferences` only reads `Reference:`/`參考:` lines, which is the right
120
+ * scope for reference *sync* — those lines are the ones UDS generates and owns.
121
+ * It is the wrong scope for "does this instruction point at anything": an older
122
+ * `uds init` left prose outside the marker block saying "優先讀取 core/ 中的精簡
123
+ * 規則(例如 core/testing-standards.md)", and on a `format: ai` project no
124
+ * `core/` directory is ever installed. An AI tool following that finds nothing,
125
+ * and nothing in the toolchain looked. Reported 2026-09-16.
126
+ *
127
+ * Existence-based, not shape-based, and deliberately so: a project that has its
128
+ * own `core/` directory is not doing anything wrong, and a rule about the shape
129
+ * of the path could not tell the two apart.
130
+ *
131
+ * @param {string} content - File content
132
+ * @param {string|null} projectPath - Project root; without it there is nothing
133
+ * to check against and the answer is "nothing", not a guess
134
+ * @returns {string[]} Distinct paths that do not resolve, in first-seen order
135
+ */
136
+ export function findBrokenPathMentions(content, projectPath) {
137
+ if (!projectPath || typeof content !== 'string') return [];
138
+
139
+ // Not preceded by `/` or a word character: that is what keeps
140
+ // `https://…/core/testing-standards.md` out of the results.
141
+ //
142
+ // The terminator class carries the full-width punctuation as well as the
143
+ // ASCII. These instructions are written in Chinese, and `(例如
144
+ // core/testing-standards.md)` ends in `)` — without it the path captured is
145
+ // `core/testing-standards.md)`, which exists nowhere and matches nothing, so
146
+ // the finding would be real and the path in it wrong.
147
+ const pattern = /(^|[^\w/\\])((?:\.standards|core)\/[^\s\n)\]`,;'")】」』〉》,。、;:!?]+)/g;
148
+ const seen = new Set();
149
+ const broken = [];
150
+
151
+ for (const match of content.matchAll(pattern)) {
152
+ const cleaned = match[2].replace(/[.,;:!?、,。]+$/, '');
153
+ if (!cleaned || seen.has(cleaned)) continue;
154
+ seen.add(cleaned);
155
+ try {
156
+ if (!existsSync(join(projectPath, cleaned))) broken.push(cleaned);
157
+ } catch {
158
+ // An unreadable path is not evidence of a broken reference.
159
+ }
160
+ }
161
+
162
+ return broken;
163
+ }
164
+
112
165
  /**
113
166
  * Get standard source paths for a category
114
167
  *
@@ -176,7 +229,27 @@ function categoryForStandard(pathOrName) {
176
229
  * - missingRefs: Standards in manifest but not referenced in integration file
177
230
  * - syncedRefs: Standards that are properly synced
178
231
  */
179
- export function compareStandardsWithReferences(manifestStandards, integrationReferences) {
232
+ export function compareStandardsWithReferences(manifestStandards, integrationReferences, context = {}) {
233
+ const { projectPath = null, options = {} } = context;
234
+
235
+ // 🔴 Existence is a separate question from adoption, and only one of the two
236
+ // was ever asked. `.standards/anti-hallucination.md` passed as "synced"
237
+ // because the manifest adopts `anti-hallucination` — while the project holds
238
+ // `anti-hallucination.ai.yaml` and the `.md` file is not on disk at all
239
+ // (reproduced on 6.9.0, 2026-09-16). An adopter's AI tool follows that link
240
+ // and finds nothing. Without a projectPath there is no disk to ask, so the
241
+ // list stays empty rather than guessing.
242
+ const danglingRefs = projectPath
243
+ ? integrationReferences.filter(ref => !existsSync(join(projectPath, '.standards', ref)))
244
+ : [];
245
+
246
+ // Options are adopted in `manifest.options`, not `manifest.standards`, so
247
+ // every `.standards/options/...` reference was reported as "not in manifest".
248
+ const optionStems = new Set(
249
+ Object.values(options || {})
250
+ .filter(v => typeof v === 'string')
251
+ .map(v => standardStem(v))
252
+ );
180
253
  // The manifest is the source of truth for what this project adopted, so ask
181
254
  // it directly. The category map below is a hand-written table covering a
182
255
  // small fraction of the standards UDS ships (compare its size against
@@ -207,6 +280,10 @@ export function compareStandardsWithReferences(manifestStandards, integrationRef
207
280
  // stem (extension- and directory-insensitive) or by legacy category name.
208
281
  const orphanedRefs = integrationReferences.filter(ref => {
209
282
  if (manifestStems.has(standardStem(ref))) return false;
283
+ if (optionStems.has(standardStem(ref))) return false;
284
+ // An option file that is on disk is adopted by definition — it got there
285
+ // because this project selected it.
286
+ if (ref.startsWith('options/') && projectPath && existsSync(join(projectPath, '.standards', ref))) return false;
210
287
  const category = categoryForStandard(ref);
211
288
  return !category || !manifestCategories.has(category);
212
289
  });
@@ -221,15 +298,34 @@ export function compareStandardsWithReferences(manifestStandards, integrationRef
221
298
  }
222
299
  const missingCategories = [...manifestCategories].filter(cat => !refCategories.has(cat));
223
300
  // Convert missing categories back to representative filenames for reporting
224
- const missingRefs = missingCategories.flatMap(cat => {
225
- const paths = CATEGORY_TO_STANDARDS[cat] || [];
226
- return paths.map(p => p.split('/').pop());
227
- });
301
+ // 🔴 `CATEGORY_TO_STANDARDS` is a hand-written table of pre-6.0.0 filenames,
302
+ // so this list named files UDS has not shipped for two majors
303
+ // (`git-workflow.md`, `error-code-standards.md`, `project-structure.md`).
304
+ // Report only what this project actually holds. With no projectPath there is
305
+ // no disk to ask, so the legacy names stand — callers that pass one get the
306
+ // filtered list.
307
+ const missingRefs = !projectPath
308
+ ? missingCategories.flatMap(cat => (CATEGORY_TO_STANDARDS[cat] || []).map(p => p.split('/').pop()))
309
+ : missingCategories.flatMap(cat => {
310
+ const paths = CATEGORY_TO_STANDARDS[cat] || [];
311
+ return paths
312
+ .map(p => p.split('/').pop())
313
+ .map(name => {
314
+ const stem = standardStem(name);
315
+ for (const candidate of [`${stem}.ai.yaml`, `${stem}.md`]) {
316
+ if (existsSync(join(projectPath, '.standards', candidate))) return candidate;
317
+ }
318
+ return null;
319
+ })
320
+ .filter(Boolean);
321
+ });
228
322
 
229
323
  // Properly synced references
230
- const syncedRefs = integrationReferences.filter(ref => !orphanedRefs.includes(ref));
324
+ const syncedRefs = integrationReferences.filter(
325
+ ref => !orphanedRefs.includes(ref) && !danglingRefs.includes(ref)
326
+ );
231
327
 
232
- return { orphanedRefs, missingRefs, syncedRefs };
328
+ return { orphanedRefs, missingRefs, syncedRefs, danglingRefs };
233
329
  }
234
330
 
235
331
  /**
@@ -320,3 +320,60 @@ export function resolveStandardFilename(entry, format = 'ai') {
320
320
  const parts = source.split(/[/\\]/);
321
321
  return parts[parts.length - 1] || null;
322
322
  }
323
+
324
+ /**
325
+ * Every filename UDS can install under `.standards/`, in either format.
326
+ *
327
+ * Built by walking the registry: each standard's own source plus every option
328
+ * choice's source, `ai` and `human` alike. Spanning both formats is deliberate
329
+ * and is why this takes no format argument — a project can hold a file in a
330
+ * format it did not install (an upgrade that changed `format`, a hand-copied
331
+ * file), and calling that "no longer shipped" would be wrong in the opposite
332
+ * direction. The question here is only ever "is this name still ours".
333
+ *
334
+ * This exists so that question has one answer, derived from the registry rather
335
+ * than from the manifest. A manifest that was never cleaned and one that was
336
+ * must not disagree about it.
337
+ *
338
+ * @returns {Set<string>} Filenames (basenames, no directory)
339
+ */
340
+ let shippableCache = null;
341
+ export function getShippableFilenames() {
342
+ if (shippableCache) return shippableCache;
343
+
344
+ const out = new Set();
345
+ const addPath = (value) => {
346
+ if (typeof value !== 'string' || value.length === 0) return;
347
+ const parts = value.split(/[/\\]/);
348
+ const name = parts[parts.length - 1];
349
+ if (name) out.add(name);
350
+ };
351
+ const addSource = (source) => {
352
+ if (!source) return;
353
+ if (typeof source === 'string') {
354
+ addPath(source);
355
+ return;
356
+ }
357
+ for (const value of Object.values(source)) addPath(value);
358
+ };
359
+
360
+ for (const std of getAllStandards()) {
361
+ addSource(std.source);
362
+ for (const category of Object.values(std.options || {})) {
363
+ for (const choice of category.choices || []) addSource(choice.source);
364
+ }
365
+ }
366
+
367
+ shippableCache = out;
368
+ return out;
369
+ }
370
+
371
+ /**
372
+ * Does UDS still ship a file by this name, in either format?
373
+ * @param {string} filename - Basename, e.g. `anti-hallucination.ai.yaml`
374
+ * @returns {boolean}
375
+ */
376
+ export function isShippedFilename(filename) {
377
+ if (typeof filename !== 'string' || filename.length === 0) return false;
378
+ return getShippableFilenames().has(filename);
379
+ }
@@ -0,0 +1,31 @@
1
+ import ora from 'ora';
2
+
3
+ /**
4
+ * A spinner that reports on stdout.
5
+ *
6
+ * ora defaults to stderr. For a spinner that redraws over itself while a pipe
7
+ * consumes stdout, that default is right. It is wrong for this CLI: these lines
8
+ * are progress reporting for a human, they sit between lines the CLI already
9
+ * prints on stdout, and in a non-TTY ora emits each one exactly once, so there
10
+ * is nothing to redraw and nothing to isolate.
11
+ *
12
+ * On Windows PowerShell 5.1 the default is worse than untidy. Any stderr output
13
+ * from a native command is wrapped in a `NativeCommandError`, so a successful
14
+ * `uds update` surfaced as an error — including the line that said it had
15
+ * succeeded. Measured on a clean run before this change: 9 lines on stdout, 2 on
16
+ * stderr, both of them the spinner, one of them `✔ 已重新產生 2 個整合檔案`.
17
+ *
18
+ * A caller that genuinely wants stderr can still ask for it; the default is the
19
+ * only thing that changes.
20
+ *
21
+ * @param {string|Object} options - Spinner text, or an ora options object
22
+ * @returns {import('ora').Ora}
23
+ */
24
+ export function createSpinner(options) {
25
+ if (typeof options === 'string') {
26
+ return ora({ text: options, stream: process.stdout });
27
+ }
28
+ return ora({ stream: process.stdout, ...options });
29
+ }
30
+
31
+ export default createSpinner;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "6.9.0",
3
+ "version": "6.10.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.9.0"
61
+ "version": "6.10.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.9.0",
68
+ "version": "6.10.0",
69
69
  "note": "Skills are now included in the main repository under skills/"
70
70
  }
71
71
  },
@@ -1091,7 +1091,7 @@
1091
1091
  "name": "Full Coverage Testing Standards",
1092
1092
  "nameZh": "全覆蓋測試標準",
1093
1093
  "source": {
1094
- "human": "core/testing-standards.md",
1094
+ "human": "core/full-coverage-testing.md",
1095
1095
  "ai": "ai/standards/full-coverage-testing.ai.yaml"
1096
1096
  },
1097
1097
  "category": "skill",
@@ -2283,7 +2283,7 @@
2283
2283
  "id": "license-compliance",
2284
2284
  "name": "License Compliance Standards",
2285
2285
  "nameZh": "授權合規標準",
2286
- "version": "6.9.0",
2286
+ "version": "6.10.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.9.0",
2298
+ "version": "6.10.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.9.0",
2310
+ "version": "6.10.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.9.0",
2322
+ "version": "6.10.0",
2323
2323
  "source": {
2324
2324
  "human": "core/resource-cost-boundary.md",
2325
2325
  "ai": "ai/standards/resource-cost-boundary.ai.yaml"