visual-spec 0.1.11 → 0.1.12

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  This repo provides a requirements analysis and delivery assistant Skill. It offers a `/vspec:*` command-driven workflow that turns raw requirements into reviewable artifacts: specs, data models, runnable prototypes, detailed design, acceptance cases, tests, and integrated implementation inputs.
2
2
 
3
- Version: 0.1.11 (2026-04-12)
3
+ Version: 0.1.12 (2026-04-12)
4
4
 
5
5
  ## Installation
6
6
 
@@ -50,7 +50,7 @@
50
50
 
51
51
  - Use when you need a deliverable Word doc for review/circulation/archiving based on current artifacts
52
52
  - Input: existing `/specs/**` artifacts (original/functions/details/models/flows, when available)
53
- - Output: `/docs/current/requirement_detail.doc` (Word-openable `.doc`, single-file HTML)
53
+ - Output: `/docs/current/requirement_detail.docx` (Word-openable `.docx`, single-file HTML)
54
54
  - Note: this Word file is a read-only summary; make changes in the corresponding markdown files and rerun `/vspec:doc` to regenerate
55
55
 
56
56
  ### 7. Solution Verification (`/vspec:verify`)
@@ -46,7 +46,7 @@
46
46
 
47
47
  - 既存成果物を集約してレビュー/回覧/保管用の Word 文書を生成する
48
48
  - 入力:既存の `/specs/**`(original/functions/details/models/flows など。存在するものを読む)
49
- - 出力:`/docs/current/requirement_detail.doc`(Word で開ける `.doc`、単一 HTML)
49
+ - 出力:`/docs/current/requirement_detail.docx`(Word で開ける `.docx`、単一 HTML)
50
50
  - 注意:この Word は要約用(直接編集しない)。修正は対応する markdown を更新し、`/vspec:doc` を再実行して再生成する
51
51
 
52
52
  ### 7. 検証(`/vspec:verify`)
@@ -57,4 +57,4 @@
57
57
  2. 如材料较多:先整理 `/docs/current/file_list.md`(按“权威性/优先级”排序)
58
58
  3. 运行 `/vspec:new`(生成 `/specs/background/original.md` 与问题清单)
59
59
  4. 需要继承 legacy/当前材料并生成新 specs:运行 `/vspec:upgrade`
60
- 5. 需要汇总成 Word:运行 `/vspec:doc` 输出到 `/docs/current/requirement_detail.doc`
60
+ 5. 需要汇总成 Word:运行 `/vspec:doc` 输出到 `/docs/current/requirement_detail.docx`
@@ -50,7 +50,7 @@
50
50
 
51
51
  - 适用场景:把当前已有的规格产物汇总成可交付的 Word 文档,用于评审/流转/归档
52
52
  - 输入:现有 `/specs/**` 产物(original/functions/details/models/flows 等,存在则读取)
53
- - 输出:`/docs/current/requirement_detail.doc`(Word 可打开的 `.doc`,HTML 单文件)
53
+ - 输出:`/docs/current/requirement_detail.docx`(Word 可打开的 `.docx`,HTML 单文件)
54
54
  - 提示:该 Word 只是汇总,不建议直接修改;修改应回到对应的 markdown 文件,修改后重新 `/vspec:doc` 生成新版本
55
55
 
56
56
  ### 7. 方案验证(`/vspec:verify`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visual-spec",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "AI skill: visual-spec (/vspec:* commands) for requirement analysis, prototyping, detailing, testing, and planning.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -47,8 +47,9 @@ Flow:
47
47
  2. 入力を raw requirement として扱う
48
48
  3. `prompts/vspec_new/background.md` を読み込み、要件分析と背景補完を実行
49
49
  4. `/specs/background/original.md` に書き込み
50
- 5. Open Questions/要確認事項の回答を促す(自己回答でも、AI の回答案→ユーザー確認でも可)
51
- 6. 以降、`prompts/vspec_new/*` に従い `/specs/` 配下の成果物を生成
50
+ 5. `/specs/background/question_and_answer.html` を生成(`prompts/vspec_new/question_and_answer.html` をそのままコピー)。このページで `original.md` / `questions.md` の質問に回答し、md に書き戻せるようにする。
51
+ 6. Open Questions/要確認事項の回答を促す(`/specs/background/question_and_answer.html` を開き、`/specs/background/original.md` を選択して回答・保存)。この時点で必ず停止して待機し、回答が揃うまで次のステップへ進まない(「続けて/继续/continue」等で回答完了を明示してもらう)。
52
+ 6. ユーザーの回答後(または「続けて/继续/continue」等で回答完了を明示した後)、`prompts/vspec_new/*` に従い `/specs/` 配下の成果物を生成
52
53
 
53
54
  ### `/vspec:refine`
54
55
 
@@ -87,7 +88,7 @@ Flow:
87
88
 
88
89
  ### `/vspec:doc`
89
90
 
90
- 要件成果物(`/specs/**`)を集約し、Word で直接開ける単一ファイルの Word 文書(HTML 形式の `.doc`)を `/docs/current/requirement_detail.doc` に生成します。
91
+ 要件成果物(`/specs/**`)を集約し、Word で直接開ける単一ファイルの Word 文書(HTML 形式の `.docx`)を `/docs/current/requirement_detail.docx` に生成します。
91
92
 
92
93
  ### `/vspec:verify`
93
94
 
@@ -42,7 +42,8 @@ description: "将原始需求分析为可评审的视觉规格,并生成相关
42
42
  3. 加载提示词文件 `prompts/vspec_new/background.md`。
43
43
  4. 使用该提示词分析需求并扩展业务背景。
44
44
  5. 将原始需求与背景分析写入 `/specs/background/original.md`。
45
- 6. 提示用户回答“待确认问题/Open Questions/要確認事項”章节中的问题(按所选语言对应章节标题)。
45
+ 5.5 同时生成交互式问答页面:`/specs/background/question_and_answer.html`(从 `prompts/vspec_new/question_and_answer.html` 复制,单文件 HTML,内联 CSS/JS),用于回答 `original.md` 与 `questions.md` 并回写。
46
+ 6. 提示用户回答“待确认问题/Open Questions/要確認事項”章节中的问题(按所选语言对应章节标题),并要求通过 `/specs/background/question_and_answer.html` 完成(在页面中选择 `/specs/background/original.md`,填写并保存回写)。然后必须暂停并等待用户回复“继续/continue/続けて”。不得继续加载后续提示词或生成后续产物(不得越过问答直接进入 stakeholders/roles/terms 等阶段)。用户明确回复后再进入下一步。
46
47
  7. 用户回复后,加载 `prompts/vspec_new/stakeholders.md` 分析干系人。
47
48
  8. 将干系人结果写入 `/specs/background/stakeholder.md`(markdown 表格)。
48
49
  9. 加载 `prompts/vspec_new/roles.md` 分析系统用户角色(直接用户)及其工作任务。
@@ -178,7 +179,7 @@ description: "将原始需求分析为可评审的视觉规格,并生成相关
178
179
  - 外部依赖:`/specs/background/dependencies.md`
179
180
  - 数据模型:`/specs/models/*.md`
180
181
  3. 加载 `prompts/vspec_doc/doc.md`,生成 Word 可打开的单文件 HTML 文档。
181
- 4. 将输出写入:`/docs/current/requirement_detail.doc`。
182
+ 4. 将输出写入:`/docs/current/requirement_detail.docx`。
182
183
 
183
184
  ### `/vspec:verify`
184
185
 
@@ -324,7 +325,7 @@ Note:Pro 版支持更广泛的质量检测能力(例如更完整的原型/
324
325
  - `prompts/vspec_new/questions.md`:生成 `/specs/background/questions.md` 的提示词。
325
326
  - `prompts/vspec_refine/refine.md`:`/vspec:refine` 使用的提示词。
326
327
  - `prompts/vspec_refine/refine_q.md`:`/vspec:refine-q` 使用的提示词。
327
- - `prompts/vspec_doc/doc.md`:`/vspec:doc` 使用的提示词,用于生成 Word 可打开的 `.doc`(HTML)需求详细文档到 `/docs/current/`。
328
+ - `prompts/vspec_doc/doc.md`:`/vspec:doc` 使用的提示词,用于生成 Word 可打开的 `.docx`(HTML)需求详细文档到 `/docs/current/`。
328
329
  - `prompts/vspec_verify/model.md`:生成 `/specs/models/*.md` 的提示词。
329
330
  - `prompts/vspec_verify/prototype.md`:生成按选栈的可运行原型工程(`/specs/prototypes/`)的提示词(必须遵循 `scheme.yaml`)。
330
331
  - `prompts/vspec_verify/validation.md`:生成 `scenario.html` 场景评审页的提示词。
@@ -49,8 +49,9 @@ Flow:
49
49
  3. Load the prompt file at `prompts/vspec_new/background.md`.
50
50
  4. Use that prompt to analyze the requirement and expand the business context.
51
51
  5. Write the raw requirement and background analysis output to `/specs/background/original.md`.
52
- 6. Ask the user to answer the questions from the Open Questions section (use the section title in the selected language).
53
- 7. After the user replies, load `prompts/vspec_new/stakeholders.md` to analyze stakeholders.
52
+ 5.5 Create `/specs/background/question_and_answer.html` by copying `prompts/vspec_new/question_and_answer.html` (single-file HTML with inline CSS/JS) so the user can answer questions and write back to markdown.
53
+ 6. Ask the user to answer the questions from the Open Questions section (use the section title in the selected language). The user should answer via `/specs/background/question_and_answer.html` (select `/specs/background/original.md` in the page and save back), then reply with a continuation signal (e.g. `继续` / `continue`). Then STOP. Do not load any subsequent prompts or generate any further artifacts before that.
54
+ 7. After the user replies (answers or confirmed), load `prompts/vspec_new/stakeholders.md` to analyze stakeholders.
54
55
  8. Write the stakeholder result to `/specs/background/stakeholder.md` (markdown table).
55
56
  9. Load `prompts/vspec_new/roles.md` to analyze system user roles (direct users) and their work tasks.
56
57
  10. Write the roles result to `/specs/background/roles.md`.
@@ -185,7 +186,7 @@ Flow:
185
186
  - Dependencies: `/specs/background/dependencies.md`
186
187
  - Models: `/specs/models/*.md`
187
188
  3. Load `prompts/vspec_doc/doc.md` and generate the doc as a Word-openable single HTML file.
188
- 4. Write the output file to: `/docs/current/requirement_detail.doc`.
189
+ 4. Write the output file to: `/docs/current/requirement_detail.docx`.
189
190
 
190
191
  ### `/vspec:verify`
191
192
 
@@ -333,7 +334,7 @@ Flow:
333
334
  - `prompts/vspec_mrd/mrd.md`: the prompt used by `/vspec:mrd` to generate market/user/competitor/product docs under `/docs/market/`.
334
335
  - `prompts/vspec_refine/refine.md`: the prompt used by `/vspec:refine` to refine the requirement based on `refine.md`.
335
336
  - `prompts/vspec_refine/refine_q.md`: the prompt used by `/vspec:refine-q` to refine the requirement based on answered questions.
336
- - `prompts/vspec_doc/doc.md`: the prompt used by `/vspec:doc` to generate a Word-openable `.doc` (HTML) requirement detail document under `/docs/current/`.
337
+ - `prompts/vspec_doc/doc.md`: the prompt used by `/vspec:doc` to generate a Word-openable `.docx` (HTML) requirement detail document under `/docs/current/`.
337
338
  - `prompts/vspec_verify/model.md`: the prompt used by `/vspec:verify` to generate `/specs/models/*.md`.
338
339
  - `prompts/vspec_verify/prototype.md`: the prompt used by `/vspec:verify` to generate the stack-selected runnable prototype under `/specs/prototypes/` (must follow `scheme.yaml`).
339
340
  - `prompts/vspec_verify/validation.md`: the prompt used by `/vspec:verify` to generate the validation web page with a `scenario.html` entry.
@@ -319,13 +319,98 @@
319
319
  font-size: 13px;
320
320
  line-height: 1.5;
321
321
  }
322
+ .tree-sep {
323
+ height: 1px;
324
+ background: var(--border);
325
+ margin: 10px 6px;
326
+ }
327
+ .outline-title {
328
+ padding: 8px 10px 2px;
329
+ color: var(--muted);
330
+ font-size: 12px;
331
+ }
332
+ .outline {
333
+ padding: 6px 6px 0;
334
+ }
335
+ .outline a {
336
+ display: block;
337
+ padding: 6px 10px;
338
+ border-radius: 10px;
339
+ color: rgba(230, 238, 252, 0.92);
340
+ text-decoration: none;
341
+ border: 1px solid transparent;
342
+ font-size: 13px;
343
+ }
344
+ .outline a:hover {
345
+ background: rgba(255, 255, 255, 0.04);
346
+ }
347
+ .outline a.active {
348
+ background: rgba(34, 197, 94, 0.12);
349
+ border-color: rgba(34, 197, 94, 0.28);
350
+ }
351
+ .file.missing {
352
+ opacity: 0.55;
353
+ cursor: not-allowed;
354
+ }
355
+ .md-toolbar {
356
+ display: flex;
357
+ flex-wrap: wrap;
358
+ gap: 8px;
359
+ align-items: center;
360
+ justify-content: space-between;
361
+ padding: 10px 12px;
362
+ border: 1px solid var(--border);
363
+ border-radius: 14px;
364
+ background: rgba(10, 16, 28, 0.82);
365
+ margin-bottom: 12px;
366
+ }
367
+ .md-toolbar .left,
368
+ .md-toolbar .right {
369
+ display: flex;
370
+ flex-wrap: wrap;
371
+ gap: 8px;
372
+ align-items: center;
373
+ }
374
+ .md-toolbar .hint {
375
+ margin: 0;
376
+ color: var(--muted);
377
+ font-size: 12px;
378
+ }
379
+ .md-split {
380
+ display: grid;
381
+ grid-template-columns: 1fr 1fr;
382
+ gap: 12px;
383
+ }
384
+ @media (max-width: 980px) {
385
+ .md-split {
386
+ grid-template-columns: 1fr;
387
+ }
388
+ }
389
+ .md-text {
390
+ width: 100%;
391
+ min-height: calc(100vh - 330px);
392
+ border-radius: 14px;
393
+ border: 1px solid var(--border);
394
+ background: rgba(10, 16, 28, 0.78);
395
+ color: var(--text);
396
+ padding: 12px;
397
+ font-size: 13px;
398
+ line-height: 1.45;
399
+ outline: none;
400
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
401
+ monospace;
402
+ }
403
+ .mode-btn.active {
404
+ background: rgba(59, 130, 246, 0.18);
405
+ border-color: rgba(59, 130, 246, 0.45);
406
+ }
322
407
  </style>
323
408
  </head>
324
409
  <body>
325
410
  <div class="top-banner">
326
411
  <div class="inner">
327
412
  <div class="msg">
328
- <b>Word export:</b> run <b>/vspec:doc</b> to generate <b>/docs/current/requirement_detail.doc</b>
413
+ <b>Word export:</b> run <b>/vspec:doc</b> to generate <b>/docs/current/requirement_detail.docx</b>
329
414
  </div>
330
415
  </div>
331
416
  </div>
@@ -344,7 +429,12 @@
344
429
  <button id="collapseAll">Collapse</button>
345
430
  </div>
346
431
  </div>
347
- <div class="tree" id="tree"></div>
432
+ <div class="tree">
433
+ <div class="outline-title">Outline</div>
434
+ <div id="outline" class="outline"></div>
435
+ <div class="tree-sep"></div>
436
+ <div id="nav"></div>
437
+ </div>
348
438
  </div>
349
439
 
350
440
  <div class="panel">
@@ -362,7 +452,7 @@
362
452
  <div class="content">
363
453
  <div class="doc" id="viewer">
364
454
  <div class="empty">
365
- Select a file from the left tree. Supported:
455
+ Select a document from the left navigation. Markdown supports editing + live preview.
366
456
  <ul>
367
457
  <li><code>.md</code> rendered as markdown</li>
368
458
  <li><code>.html</code> rendered via iframe</li>
@@ -381,7 +471,8 @@
381
471
  const PLANTUML_SERVER = "https://www.plantuml.com/plantuml/svg/";
382
472
 
383
473
  const dom = {
384
- tree: document.getElementById("tree"),
474
+ nav: document.getElementById("nav"),
475
+ outline: document.getElementById("outline"),
385
476
  search: document.getElementById("search"),
386
477
  fileCount: document.getElementById("fileCount"),
387
478
  viewer: document.getElementById("viewer"),
@@ -396,6 +487,9 @@
396
487
  paths: [],
397
488
  activePath: null,
398
489
  activeButton: null,
490
+ handles: new Map(),
491
+ activeHeadings: [],
492
+ activeHeadingId: null,
399
493
  };
400
494
 
401
495
  function escapeHtml(str) {
@@ -446,6 +540,45 @@
446
540
  return s.length ? s[s.length - 1] : path;
447
541
  }
448
542
 
543
+ function firstHeadingTitle(md) {
544
+ const m = String(md || "").match(/^#{1,6}\s+(.+)\s*$/m);
545
+ return m ? m[1].trim() : "";
546
+ }
547
+
548
+ function displayTitle(path) {
549
+ const t = fileType(path);
550
+ if (t === "Markdown") {
551
+ const title = firstHeadingTitle(state.files[path] || "");
552
+ return title || basename(path);
553
+ }
554
+ return basename(path);
555
+ }
556
+
557
+ function slugify(text) {
558
+ return String(text || "")
559
+ .trim()
560
+ .toLowerCase()
561
+ .replace(/[`"'<>]/g, "")
562
+ .replace(/\s+/g, "-")
563
+ .replace(/[^a-z0-9\u4e00-\u9fff\u3040-\u30ff_-]/g, "");
564
+ }
565
+
566
+ function extractHeadings(md) {
567
+ const lines = String(md || "").replace(/\r\n/g, "\n").split("\n");
568
+ const out = [];
569
+ let idx = 0;
570
+ for (const line of lines) {
571
+ const m = line.match(/^(#{1,6})\s+(.+)\s*$/);
572
+ if (!m) continue;
573
+ idx++;
574
+ const level = m[1].length;
575
+ const text = m[2].trim();
576
+ const id = `h-${slugify(text) || "section"}-${idx}`;
577
+ out.push({ level, text, id });
578
+ }
579
+ return out;
580
+ }
581
+
449
582
  function filterMatch(filterLower, label, path) {
450
583
  if (!filterLower) return true;
451
584
  return `${label || ""} ${path || ""}`.toLowerCase().includes(filterLower);
@@ -524,13 +657,23 @@
524
657
  chapters.push({
525
658
  label: "Requirement",
526
659
  children: [
527
- original ? { type: "file", label: "Original Requirement", path: original } : null,
528
- stakeholder ? { type: "file", label: "Stakeholders", path: stakeholder } : null,
529
- roles ? { type: "file", label: "Roles", path: roles } : null,
530
- terms ? { type: "file", label: "Terms", path: terms } : null,
531
- scenarios ? { type: "file", label: "Scenarios", path: scenarios } : null,
532
- dependencies ? { type: "file", label: "Dependencies", path: dependencies } : null,
533
- questions ? { type: "file", label: "Questions", path: questions } : null,
660
+ { type: "file", label: "Original Requirement", path: original || "/specs/background/original.md", missing: !original },
661
+ {
662
+ type: "file",
663
+ label: "Stakeholders",
664
+ path: stakeholder || "/specs/background/stakeholders.md",
665
+ missing: !stakeholder,
666
+ },
667
+ { type: "file", label: "Roles", path: roles || "/specs/background/roles.md", missing: !roles },
668
+ { type: "file", label: "Terms", path: terms || "/specs/background/terms.md", missing: !terms },
669
+ { type: "file", label: "Scenarios", path: scenarios || "/specs/background/scenarios.md", missing: !scenarios },
670
+ {
671
+ type: "file",
672
+ label: "Dependencies",
673
+ path: dependencies || "/specs/background/dependencies.md",
674
+ missing: !dependencies,
675
+ },
676
+ { type: "file", label: "Questions", path: questions || "/specs/background/questions.md", missing: !questions },
534
677
  ].filter(Boolean),
535
678
  });
536
679
 
@@ -553,7 +696,7 @@
553
696
 
554
697
  chapters.push({
555
698
  label: "Functions",
556
- children: functions.map((p) => ({ type: "file", label: basename(p), path: p })),
699
+ children: functions.map((p) => ({ type: "file", label: displayTitle(p), path: p })),
557
700
  });
558
701
 
559
702
  chapters.push({
@@ -563,7 +706,7 @@
563
706
 
564
707
  chapters.push({
565
708
  label: "Models",
566
- children: models.map((p) => ({ type: "file", label: basename(p), path: p })),
709
+ children: models.map((p) => ({ type: "file", label: displayTitle(p), path: p })),
567
710
  });
568
711
 
569
712
  return chapters.filter((c) => (c.children || []).length);
@@ -581,8 +724,14 @@
581
724
  if (!filterMatch(filterLower, node.label, node.path)) return;
582
725
  const btn = document.createElement("button");
583
726
  btn.className = "file";
584
- btn.textContent = node.label;
585
- btn.addEventListener("click", () => openFile(node.path, btn));
727
+ const title = exists(node.path) ? displayTitle(node.path) : node.label;
728
+ btn.textContent = title;
729
+ if (!exists(node.path) || node.missing) {
730
+ btn.classList.add("missing");
731
+ btn.disabled = true;
732
+ } else {
733
+ btn.addEventListener("click", () => openFile(node.path, btn));
734
+ }
586
735
  if (state.activePath === node.path) btn.classList.add("active");
587
736
  container.appendChild(btn);
588
737
  return;
@@ -602,11 +751,40 @@
602
751
  }
603
752
 
604
753
  function renderSidebar() {
605
- dom.tree.innerHTML = "";
754
+ dom.nav.innerHTML = "";
606
755
  const filterLower = (dom.search.value || "").trim().toLowerCase();
607
756
  const chapters = buildChapters();
608
757
  for (const ch of chapters) {
609
- renderNavNode({ type: "group", label: ch.label, children: ch.children }, dom.tree, filterLower);
758
+ renderNavNode({ type: "group", label: ch.label, children: ch.children }, dom.nav, filterLower);
759
+ }
760
+ }
761
+
762
+ function renderOutline(headings) {
763
+ dom.outline.innerHTML = "";
764
+ const hs = (headings || []).filter((h) => h.level >= 1 && h.level <= 4);
765
+ if (!state.activePath || fileType(state.activePath) !== "Markdown" || !hs.length) {
766
+ const a = document.createElement("a");
767
+ a.href = "javascript:void(0)";
768
+ a.textContent = "—";
769
+ a.style.cursor = "default";
770
+ dom.outline.appendChild(a);
771
+ return;
772
+ }
773
+ for (const h of hs) {
774
+ const a = document.createElement("a");
775
+ a.href = "javascript:void(0)";
776
+ a.textContent = h.text;
777
+ a.style.paddingLeft = `${10 + (h.level - 1) * 12}px`;
778
+ if (state.activeHeadingId === h.id) a.classList.add("active");
779
+ a.addEventListener("click", () => {
780
+ const el = document.getElementById(h.id);
781
+ if (el) {
782
+ state.activeHeadingId = h.id;
783
+ el.scrollIntoView({ block: "start", behavior: "smooth" });
784
+ renderOutline(state.activeHeadings);
785
+ }
786
+ });
787
+ dom.outline.appendChild(a);
610
788
  }
611
789
  }
612
790
 
@@ -762,9 +940,15 @@
762
940
  async function renderMarkdown(md) {
763
941
  const root = document.createElement("div");
764
942
  const blocks = parseMarkdown(md);
943
+ let hIndex = 0;
944
+ const headings = [];
765
945
  for (const b of blocks) {
766
946
  if (b.type === "heading") {
947
+ hIndex++;
948
+ const id = `h-${slugify(b.text) || "section"}-${hIndex}`;
949
+ headings.push({ level: b.level, text: b.text, id });
767
950
  const h = document.createElement("h" + b.level);
951
+ h.id = id;
768
952
  h.innerHTML = renderInline(b.text);
769
953
  root.appendChild(h);
770
954
  continue;
@@ -849,10 +1033,46 @@
849
1033
  continue;
850
1034
  }
851
1035
  }
1036
+ state.activeHeadings = headings;
1037
+ state.activeHeadingId = null;
852
1038
  return root;
853
1039
  }
854
1040
 
1041
+ async function linkLocalFileForWriteBack(path) {
1042
+ if (!("showOpenFilePicker" in window)) {
1043
+ alert("Browser does not support File System Access API. Use Download instead.");
1044
+ return;
1045
+ }
1046
+ const [handle] = await window.showOpenFilePicker({
1047
+ multiple: false,
1048
+ types: [{ description: "Files", accept: { "text/plain": [".md", ".puml", ".txt", ".html"] } }],
1049
+ });
1050
+ state.handles.set(path, handle);
1051
+ }
1052
+
1053
+ async function saveToLinkedHandle(path, text) {
1054
+ const handle = state.handles.get(path);
1055
+ if (!handle) return false;
1056
+ const writable = await handle.createWritable();
1057
+ await writable.write(text);
1058
+ await writable.close();
1059
+ return true;
1060
+ }
1061
+
1062
+ function downloadText(filename, text, type) {
1063
+ const blob = new Blob([text], { type: type || "text/plain;charset=utf-8" });
1064
+ const url = URL.createObjectURL(blob);
1065
+ const a = document.createElement("a");
1066
+ a.href = url;
1067
+ a.download = filename;
1068
+ document.body.appendChild(a);
1069
+ a.click();
1070
+ a.remove();
1071
+ URL.revokeObjectURL(url);
1072
+ }
1073
+
855
1074
  async function openFile(path, btn) {
1075
+ if (!exists(path)) return;
856
1076
  const content = state.files[path];
857
1077
  setActive(path, btn);
858
1078
  dom.viewer.innerHTML = "";
@@ -881,8 +1101,121 @@
881
1101
  }
882
1102
  return;
883
1103
  }
1104
+ if (t === "Markdown") {
1105
+ const toolbar = document.createElement("div");
1106
+ toolbar.className = "md-toolbar";
1107
+ const left = document.createElement("div");
1108
+ left.className = "left";
1109
+ const right = document.createElement("div");
1110
+ right.className = "right";
1111
+
1112
+ const modeSplit = document.createElement("button");
1113
+ modeSplit.className = "mode-btn active";
1114
+ modeSplit.textContent = "Split";
1115
+ const modeEdit = document.createElement("button");
1116
+ modeEdit.className = "mode-btn";
1117
+ modeEdit.textContent = "Edit";
1118
+ const modePreview = document.createElement("button");
1119
+ modePreview.className = "mode-btn";
1120
+ modePreview.textContent = "Preview";
1121
+
1122
+ const linkBtn = document.createElement("button");
1123
+ linkBtn.textContent = "Link File";
1124
+ linkBtn.addEventListener("click", async () => {
1125
+ try {
1126
+ await linkLocalFileForWriteBack(path);
1127
+ alert("Linked. You can Save back now.");
1128
+ } catch (e) {}
1129
+ });
1130
+
1131
+ const saveBtn = document.createElement("button");
1132
+ saveBtn.className = "primary";
1133
+ saveBtn.textContent = "Save";
1134
+
1135
+ const dlBtn = document.createElement("button");
1136
+ dlBtn.textContent = "Download";
1137
+
1138
+ const hint = document.createElement("div");
1139
+ hint.className = "hint";
1140
+ hint.textContent = "Edit markdown and preview on the right. Save requires linking a local file.";
1141
+
1142
+ left.appendChild(modeSplit);
1143
+ left.appendChild(modeEdit);
1144
+ left.appendChild(modePreview);
1145
+ left.appendChild(hint);
1146
+ right.appendChild(linkBtn);
1147
+ right.appendChild(saveBtn);
1148
+ right.appendChild(dlBtn);
1149
+ toolbar.appendChild(left);
1150
+ toolbar.appendChild(right);
1151
+
1152
+ const split = document.createElement("div");
1153
+ split.className = "md-split";
1154
+ const ta = document.createElement("textarea");
1155
+ ta.className = "md-text";
1156
+ ta.value = content || "";
1157
+ const previewWrap = document.createElement("div");
1158
+ previewWrap.className = "doc";
1159
+ split.appendChild(ta);
1160
+ split.appendChild(previewWrap);
1161
+
1162
+ let timer = null;
1163
+ async function refresh() {
1164
+ previewWrap.innerHTML = "";
1165
+ const rendered = await renderMarkdown(ta.value || "");
1166
+ previewWrap.appendChild(rendered);
1167
+ renderOutline(state.activeHeadings);
1168
+ }
1169
+ await refresh();
1170
+ ta.addEventListener("input", () => {
1171
+ if (timer) clearTimeout(timer);
1172
+ timer = setTimeout(() => refresh(), 180);
1173
+ });
1174
+
1175
+ function setMode(mode) {
1176
+ modeSplit.classList.remove("active");
1177
+ modeEdit.classList.remove("active");
1178
+ modePreview.classList.remove("active");
1179
+ if (mode === "split") modeSplit.classList.add("active");
1180
+ if (mode === "edit") modeEdit.classList.add("active");
1181
+ if (mode === "preview") modePreview.classList.add("active");
1182
+ if (mode === "split") {
1183
+ ta.style.display = "";
1184
+ previewWrap.style.display = "";
1185
+ } else if (mode === "edit") {
1186
+ ta.style.display = "";
1187
+ previewWrap.style.display = "none";
1188
+ } else {
1189
+ ta.style.display = "none";
1190
+ previewWrap.style.display = "";
1191
+ }
1192
+ }
1193
+ modeSplit.addEventListener("click", () => setMode("split"));
1194
+ modeEdit.addEventListener("click", () => setMode("edit"));
1195
+ modePreview.addEventListener("click", () => setMode("preview"));
1196
+
1197
+ saveBtn.addEventListener("click", async () => {
1198
+ try {
1199
+ const ok = await saveToLinkedHandle(path, ta.value || "");
1200
+ if (!ok) alert("No linked file. Click Link File first, or use Download.");
1201
+ else alert("Saved.");
1202
+ } catch (e) {
1203
+ alert("Save failed.");
1204
+ }
1205
+ });
1206
+ dlBtn.addEventListener("click", () => {
1207
+ downloadText(basename(path), ta.value || "", "text/markdown;charset=utf-8");
1208
+ });
1209
+
1210
+ dom.viewer.className = "";
1211
+ dom.viewer.appendChild(toolbar);
1212
+ dom.viewer.appendChild(split);
1213
+ return;
1214
+ }
1215
+ dom.viewer.className = "doc";
884
1216
  const rendered = await renderMarkdown(content || "");
885
1217
  dom.viewer.appendChild(rendered);
1218
+ renderOutline(state.activeHeadings);
886
1219
  }
887
1220
 
888
1221
  function setFiles(files) {
@@ -892,6 +1225,7 @@
892
1225
  .sort((a, b) => a.localeCompare(b));
893
1226
  dom.fileCount.textContent = `${state.paths.length} files`;
894
1227
  renderSidebar();
1228
+ renderOutline([]);
895
1229
  const fromHash = decodeURIComponent((location.hash || "").replace(/^#/, "")) || "";
896
1230
  if (fromHash && state.files[fromHash] != null) {
897
1231
  openFile(fromHash, null);
@@ -913,7 +1247,7 @@
913
1247
  } catch (e) {}
914
1248
  });
915
1249
  dom.collapseAll.addEventListener("click", () => {
916
- const ds = dom.tree.querySelectorAll("details");
1250
+ const ds = dom.nav.querySelectorAll("details");
917
1251
  for (const d of ds) d.open = false;
918
1252
  });
919
1253
 
@@ -19,8 +19,12 @@
19
19
  2. HTML 必须是完整单文件(包含内联 CSS 与内联 JS),不得依赖外部脚本/样式资源
20
20
  2.1 为了保证生成稳定性:`/specs/details/index.html` 必须以模板为基础生成——从 `prompts/vspec_detail/index.html` 复制整份内容作为起始模板,然后仅做必要的替换/填充(例如内嵌文件内容 JSON、默认打开文件等)。不要从零开始“自由发挥”生成 HTML。
21
21
  3. 页面布局:
22
- - 左侧:章节导航(按“章节结构”分组,而不是按文件名;必须包含并链接到:原始需求、干系人、角色、术语、场景、功能清单,以及 details 下的各类细节文档)
23
- - 右侧:阅读窗口(支持标题、列表、表格、代码块等基础 Markdown 渲染)
22
+ - 左侧:章节导航 + 标题层级目录(必须)
23
+ - 章节导航按“章节结构”分组,而不是按文件名;必须包含并链接到:背景(original.md)、干系人(stakeholders.md)、角色(roles.md)、术语(terms.md)、流程(flows)、场景(scenarios)、数据模型(models)、功能清单(functions)、以及 details 下的各类细节文档
24
+ - 对当前选中的 Markdown 文档:左侧必须展示该文档的“标题与层级结构目录”(基于 `#`~`####`),可点击跳转到右侧预览对应标题位置
25
+ - 右侧:Markdown Editor(必须)
26
+ - 必须支持在线编辑与预览(至少提供 Split/Edit/Preview 三种模式)
27
+ - 支持把编辑结果保存回本地文件(若浏览器不支持写入,则提供下载替换的兼容方案)
24
28
  4. 渲染规则:
25
29
  - `*.md`:以 Markdown 渲染方式显示(不是纯文本)
26
30
  - `*.html`:使用 `iframe srcdoc` 渲染(而不是当作文本显示)
@@ -39,7 +43,7 @@
39
43
  - 支持通过 URL hash 直接打开文件(例如 `index.html#module/a/b.md`)
40
44
  7. 文档导出提示(必须):
41
45
  - 页面顶部必须有一个明显提示条(banner/notice),说明:可通过 `/vspec:doc` 生成 Word 版需求文档
42
- - 提示条需要包含输出路径:`/docs/current/requirement_detail.doc`
46
+ - 提示条需要包含输出路径:`/docs/current/requirement_detail.docx`
43
47
  - 提示条文案要简短、醒目,不随目录滚动消失(建议固定在顶部)
44
48
 
45
49
  实现约束(必须):
@@ -1,4 +1,4 @@
1
- 你是一名资深产品经理与交付文档编辑。你的任务是:把项目已有的需求产物(`/specs/**`)汇总成一份可直接用 Microsoft Word 打开的“需求详细文档”,并输出为 Word 可识别的单文件 `.doc`(HTML 格式),写入到 `/docs/current/` 目录下。
1
+ 你是一名资深产品经理与交付文档编辑。你的任务是:把项目已有的需求产物(`/specs/**`)汇总成一份可直接用 Microsoft Word 打开的“需求详细文档”,并输出为 Word 可识别的单文件 `.docx`(HTML 格式),写入到 `/docs/current/` 目录下。
2
2
 
3
3
  语言与本地化(必须):
4
4
  - 读取 `/scheme.yaml` 的 `selected.language`(支持 `en`、`zh`、`ja`;若缺失/非法则按 `en` 处理;`zh-CN` 视为 `zh` 的别名)
@@ -16,8 +16,9 @@
16
16
 
17
17
  输出与写入要求(必须):
18
18
  1. 确保目录 `/docs/current/` 存在(若不存在则创建)
19
- 2. 仅写入 1 个 Word 文档文件(路径固定):`/docs/current/requirement_detail.doc`
19
+ 2. 仅写入 1 个 Word 文档文件(路径固定):`/docs/current/requirement_detail.docx`
20
20
  3. 该文件必须是完整的 HTML 文档(包含 `<!doctype html>`、`<html>`、`<head>`、`<body>`),并使用内联 CSS(不得依赖外部资源)
21
+ - 若 Word 弹出“文件格式与扩展名不匹配”的提示:允许打开(该文件内容为 HTML,扩展名为 `.docx`,用于便于传阅与归档)
21
22
  4. 文件内容必须保证在 Word 中可打开且排版可读:
22
23
  - 使用清晰的层级标题(H1/H2/H3)
23
24
  - 表格必须有边框与表头样式
@@ -40,7 +41,7 @@
40
41
  1. 封面
41
42
  - 文档标题:需求详细文档 / Requirement Detail / 要件詳細ドキュメント
42
43
  - 项目名称(若原始需求中可推断则填写;否则留空)
43
- - 版本号:固定写 `0.1.11`
44
+ - 版本号:固定写 `0.1.12`
44
45
  - 生成日期:使用今天日期(YYYY-MM-DD)
45
46
  2. 目录(手工目录即可)
46
47
  - 以可点击的锚点链接到各章节
@@ -23,6 +23,7 @@
23
23
  - 边界与范围(包含/不包含、上下限、默认值、空值、异常)
24
24
  - 状态与操作(暂停/继续/撤回/取消/驳回/变更/紧急叫停等的口径)
25
25
  - 数据口径(字段含义、单位、精度、舍入、时间口径、唯一性)
26
+ - 操作体验与交互模式(一步一步向导 vs 一次性表格/表单、列表/详情/批量操作、行内编辑/弹窗/抽屉、草稿/自动保存/撤销、Web 与 Mobile 的差异)
26
27
  - 权限与数据权限(角色、组织范围、越权处理、审计)
27
28
  - 外部依赖(系统职责边界、接口清单、失败策略、幂等、对账与补偿)
28
29
  - 资料与模板(导入/导出模板、通知文案、协议、证书等)
@@ -90,3 +91,7 @@
90
91
  - 建议的回答方式(在 `questions.md` 逐条填写“回答/回答者/回答时间/状态”)
91
92
  - 填写完成后执行 `/vspec:refine-q` 合并答案进入 `original.md`
92
93
  - 若暂时无法回答:允许先保留未回答,但需要标注原因/预计时间(写在“回答”里即可)
94
+ 5. 同时写入固定的 HTML 交互问答页面(用于更容易回答并回写 md 文件):
95
+ - 写入:`/specs/background/question_and_answer.html`
96
+ - 该 HTML 必须为完整可直接打开的单文件(包含内联 CSS 与 JS),无需外部资源
97
+ - HTML 内容要求:从 `prompts/vspec_more_q/question_and_answer.html` 复制(保持一致),用于读取/编辑 `original.md` 与 `questions.md` 并回写
@@ -542,7 +542,13 @@ catalog:
542
542
  写入要求:
543
543
  - 将本次完整输出(包含“原始需求、分析内容、待确认问题”)追加写入到:`/specs/background/original.md`
544
544
  - 文件中必须保留原始需求原文与本次分析结果,便于后续 stakeholders/roles 阶段引用
545
+ - 同时写入固定的 HTML 交互问答页面(用于回答 `original.md` 中的“待确认问题/Open Questions/要確認事項”,并可回写 markdown):
546
+ - 写入:`/specs/background/question_and_answer.html`
547
+ - 该 HTML 必须为完整可直接打开的单文件(包含内联 CSS 与 JS),无需外部资源
548
+ - HTML 内容要求:从 `prompts/vspec_new/question_and_answer.html` 复制(保持一致)
545
549
  - 交互提示(必须在对话中输出;不要写入 `/specs/background/original.md`):
550
+ - 重要:在你输出并写入 `original.md` 之后,必须立刻停止;不要继续生成 stakeholders/roles/terms/flows/scenarios 等后续产物。必须等待用户先把“待确认问题/Open Questions/要確認事項”回答完毕(或用户明确回复“继续/continue/続けて”表示已完成问答)后,才能进入下一步。
551
+ - 告知用户:请打开 `/specs/background/question_and_answer.html`,选择 `/specs/background/original.md`,在 HTML 表单中回答“待确认问题”并保存回写;完成后再回复“继续/continue/続けて”
546
552
  - 告知用户:回答“待确认问题”可以自己逐条回答,也可以委托 AI 基于当前信息先给出一版“建议答案”,用户再逐条确认/修改
547
553
  - 给出可复制的建议话术(按所选语言输出对应版本):
548
554
  - 语言=en:`Please propose suggested answers for the Open Questions based on current information. I will confirm or edit each answer.`
@@ -16,6 +16,7 @@
16
16
  - 角色与权限(谁能做什么,审批链路,越权处理)
17
17
  - 数据与口径(关键字段、主数据来源、术语歧义)
18
18
  - 流程与场景(取消/变更/驳回/紧急叫停/换人执行等)
19
+ - 操作体验与交互模式(必须明确“如何操作”而不只是“有什么功能”):一步一步向导 vs 一次性表格/表单、列表/详情/批量操作、批量编辑与导入导出、表格行内编辑/弹窗/抽屉、是否支持草稿/自动保存/撤销、移动端与 Web 的差异体验
19
20
  - 约束与合规(管理制度、法律法规、留痕留存)
20
21
  - 外部依赖(对接系统、数据方向、失败处理、时效)
21
22
  - 交付物资料(需要业务提供的文档、模板、文案、协议等)
@@ -87,3 +88,14 @@
87
88
 
88
89
  4. 编号从 1 开始递增
89
90
  5. 提问者默认填“BA/系统分析”
91
+ 6. 操作体验强制覆盖(必须):
92
+ - 至少生成 4 条与“操作体验/交互模式”相关的问题,且必须覆盖以下主题中的至少 3 个:
93
+ - 一步一步向导(Wizard)还是一次性表格/表单展示(Single-page)
94
+ - 核心列表是否需要支持批量操作(批量通过/批量驳回/批量分派/批量导出等)
95
+ - 表格编辑方式:行内编辑 vs 弹窗/抽屉;是否需要批量编辑
96
+ - 草稿/自动保存/恢复草稿/撤销(Undo)与二次确认(Confirm)的口径
97
+ - Web 与 Mobile 的差异:哪些步骤必须在移动端完成、哪些在 Web 完成、是否需要“只读提示/置灰”
98
+ 6. 同时写入固定的 HTML 交互问答页面(用于更容易回答并回写 md 文件):
99
+ - 写入:`/specs/background/question_and_answer.html`
100
+ - 该 HTML 必须为完整可直接打开的单文件(包含内联 CSS 与 JS),无需外部资源
101
+ - HTML 内容要求:从 `prompts/vspec_new/question_and_answer.html` 复制(保持一致),用于读取/编辑 `original.md` 与 `questions.md` 并回写