@oevery/rsp 3.1.0-beta.3 → 3.1.0-beta.4

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.
@@ -215,6 +215,162 @@ async function validateDecisionRecordsFilesystemPath(pathValue) {
215
215
  return null;
216
216
  }
217
217
 
218
+ // src/core/document-model.ts
219
+ var CHANGE_DOCUMENT_SCHEMA = {
220
+ titlePrefix: "# Change:",
221
+ sections: [
222
+ { id: "proposal", heading: "Proposal" },
223
+ { id: "spec", heading: "Spec" },
224
+ { id: "design", heading: "Design" },
225
+ { id: "tasks", heading: "Tasks" },
226
+ { id: "verify", heading: "Verify" },
227
+ { id: "blockers", heading: "Blockers" }
228
+ ]
229
+ };
230
+ var GROUP_BRIEF_DOCUMENT_SCHEMA = {
231
+ titlePrefix: "# Change Group:",
232
+ sections: [
233
+ { id: "goal", heading: "Goal" },
234
+ { id: "scope", heading: "Scope" },
235
+ { id: "sharedConstraints", heading: "Shared Constraints" },
236
+ { id: "slices", heading: "Slices" },
237
+ { id: "completionConditions", heading: "Completion Conditions" },
238
+ { id: "durableOutcomes", heading: "Durable Outcomes" },
239
+ { id: "blockers", heading: "Blockers" }
240
+ ]
241
+ };
242
+ var DocumentSectionCardinalityError = class extends Error {
243
+ constructor(sectionId, count) {
244
+ super(`document must contain exactly one ${sectionId} section; found ${count}`);
245
+ this.sectionId = sectionId;
246
+ this.count = count;
247
+ this.name = "DocumentSectionCardinalityError";
248
+ }
249
+ sectionId;
250
+ count;
251
+ };
252
+ function parseRspDocument(source, schema) {
253
+ const definitionsByHeading = new Map(schema.sections.map((definition) => [definition.heading, definition]));
254
+ const observedHeadings = collectLevelTwoHeadings(source);
255
+ const sections = /* @__PURE__ */ new Map();
256
+ const unknownHeadings = [];
257
+ for (let index = 0; index < observedHeadings.length; index++) {
258
+ const observed = observedHeadings[index];
259
+ const definition = definitionsByHeading.get(observed.heading);
260
+ if (!definition) {
261
+ unknownHeadings.push(observed.heading);
262
+ continue;
263
+ }
264
+ const nextHeading = observedHeadings[index + 1];
265
+ const bodyEnd = nextHeading ? lineBreakStart(source, nextHeading.start) : source.length;
266
+ const section = {
267
+ id: definition.id,
268
+ heading: definition.heading,
269
+ canonical: observed.canonical,
270
+ body: source.slice(observed.end, bodyEnd).trim(),
271
+ headingStart: observed.start,
272
+ headingEnd: observed.end,
273
+ bodyStart: observed.end,
274
+ bodyEnd
275
+ };
276
+ const matches = sections.get(definition.id) ?? [];
277
+ matches.push(section);
278
+ sections.set(definition.id, matches);
279
+ }
280
+ const missingSections = schema.sections.filter((definition) => !sections.has(definition.id)).map((definition) => definition.id);
281
+ const duplicateSections = schema.sections.filter((definition) => (sections.get(definition.id)?.length ?? 0) > 1).map((definition) => definition.id);
282
+ const titleOccurrences = collectDocumentTitles(source, schema.titlePrefix);
283
+ const titles = titleOccurrences.map((title) => title.value);
284
+ return {
285
+ source,
286
+ schema,
287
+ title: titles[0] ?? null,
288
+ titles,
289
+ titleOccurrences,
290
+ sections,
291
+ unknownHeadings,
292
+ missingSections,
293
+ duplicateSections
294
+ };
295
+ }
296
+ function getDocumentTitles(document, compatibility = "broad") {
297
+ if (compatibility === "broad")
298
+ return document.titles;
299
+ return document.titleOccurrences.filter((title) => compatibility === "identity" ? title.identityCompatible : title.spaced).map((title) => title.value);
300
+ }
301
+ function getDocumentTitle(document, compatibility = "broad") {
302
+ return getDocumentTitles(document, compatibility)[0] ?? null;
303
+ }
304
+ function getDocumentSections(document, sectionId) {
305
+ return document.sections.get(sectionId) ?? [];
306
+ }
307
+ function getDocumentSection(document, sectionId) {
308
+ return getDocumentSections(document, sectionId)[0];
309
+ }
310
+ function getDocumentSectionBody(document, sectionId) {
311
+ return getDocumentSection(document, sectionId)?.body ?? "";
312
+ }
313
+ function getDocumentSectionDefinitionByHeading(schema, heading) {
314
+ return schema.sections.find((candidate) => candidate.heading === heading);
315
+ }
316
+ function getCanonicalSectionHeadings(schema) {
317
+ return schema.sections.map((section) => section.heading);
318
+ }
319
+ function renderDocumentSectionHeading(schema, sectionId) {
320
+ const definition = schema.sections.find((candidate) => candidate.id === sectionId);
321
+ if (!definition)
322
+ throw new Error(`unknown document section: ${sectionId}`);
323
+ return `## ${definition.heading}`;
324
+ }
325
+ function renderDocumentTitle(schema, title) {
326
+ return `${schema.titlePrefix} ${title}`;
327
+ }
328
+ function appendDocumentSectionItem(document, sectionId, item) {
329
+ const matches = getDocumentSections(document, sectionId);
330
+ if (matches.length !== 1)
331
+ throw new DocumentSectionCardinalityError(sectionId, matches.length);
332
+ const insertAt = matches[0].bodyEnd;
333
+ const before = document.source.slice(0, insertAt);
334
+ const separator = before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n";
335
+ return `${before}${separator}${item}
336
+ ${document.source.slice(insertAt)}`;
337
+ }
338
+ function collectLevelTwoHeadings(source) {
339
+ const headings = [];
340
+ const pattern = /^## ([^\r\n]+)\r?$/gm;
341
+ let match;
342
+ while ((match = pattern.exec(source)) !== null) {
343
+ const heading = match[1].trimEnd();
344
+ headings.push({
345
+ heading,
346
+ canonical: heading === match[1],
347
+ start: match.index,
348
+ end: match.index + match[0].length
349
+ });
350
+ }
351
+ return headings;
352
+ }
353
+ function collectDocumentTitles(source, prefix) {
354
+ const titles = [];
355
+ for (const line of source.split(/\r?\n/)) {
356
+ const trimmed = line.trim();
357
+ if (!trimmed.startsWith(prefix))
358
+ continue;
359
+ const identity = line.startsWith(prefix) ? line.slice(prefix.length).match(/^\s+(\S+)\s*$/) : null;
360
+ titles.push({
361
+ value: trimmed.slice(prefix.length).trim(),
362
+ identityCompatible: identity !== null,
363
+ spaced: line.startsWith(`${prefix} `)
364
+ });
365
+ }
366
+ return titles;
367
+ }
368
+ function lineBreakStart(source, headingStart) {
369
+ if (headingStart > 0 && source[headingStart - 1] === "\n")
370
+ return headingStart - 1;
371
+ return headingStart;
372
+ }
373
+
218
374
  // src/core/helpers.ts
219
375
  import { existsSync as existsSync2 } from "fs";
220
376
  import { readdir as readdir3, readFile, rmdir } from "fs/promises";
@@ -314,71 +470,64 @@ function normalizeLogicalPath(pathValue) {
314
470
  return pathValue.split(sep2).join("/").replace(/\\/g, "/");
315
471
  }
316
472
  function generateChangeContent(name, summary = "", kind, options = {}) {
317
- const proposalSummary = summary || (name === "project-setup" ? "Capture the project model, boundaries, and stable local constraints" : "<one-line summary>");
473
+ const placeholder = "<\u2026>";
474
+ const proposalSummary = summary || placeholder;
318
475
  if (name === "project-setup") {
319
476
  return `---
320
477
  kind: ops
321
478
  ---
322
479
 
323
- # Change: project-setup
480
+ ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, "project-setup")}
324
481
 
325
- ## Proposal
482
+ ${changeSectionHeading("proposal")}
326
483
  - Outcome: ${proposalSummary}
327
484
  - Why:
328
- - Establish a durable project model before normal implementation work starts
485
+ - ${placeholder}
329
486
  - Scope:
330
- - Review the repository structure, entrypoints, and primary outputs
331
- - Fill .rsp/specs/design.md with durable architecture facts
332
- - Add stable navigation, context, instructions, or validation steps to project-owned CONTEXT.md or the nearest AGENTS.md when those conventions exist
487
+ - ${placeholder}
333
488
  - Non-goals:
334
- - Do not duplicate durable project facts across this change, specs, and AGENTS.md
489
+ - ${placeholder}
335
490
 
336
- ## Spec
491
+ ${changeSectionHeading("spec")}
337
492
  ### ADDED
338
- - Requirement: project bootstrap capture
339
- - The repository's purpose, scope, and structure are reflected in .rsp/specs/design.md
340
-
341
- ### MODIFIED
342
- - Requirement: stable project-owned context and instructions
343
- - Stable navigation and context are reflected in project-owned CONTEXT.md when that convention exists
344
- - Stable agent instructions are reflected in the nearest project-owned AGENTS.md when needed
493
+ - Requirement: ${placeholder}
494
+ - ${placeholder}
345
495
 
346
496
  ### Acceptance
347
- #### Scenario: project model captured
348
- - GIVEN an initialized RSP project
349
- - WHEN project setup is completed
350
- - THEN .rsp/specs/design.md reflects durable project facts
351
- - AND project-owned CONTEXT.md and the nearest AGENTS.md contain only the stable context and instructions assigned to them by Host Project conventions
497
+ #### Scenario: ${placeholder}
498
+ - GIVEN ${placeholder}
499
+ - WHEN ${placeholder}
500
+ - THEN ${placeholder}
352
501
 
353
- ## Design
502
+ ${changeSectionHeading("design")}
354
503
  - Approach:
355
- - Keep bootstrap work in this single Change while returning durable outcomes to their existing project owners
504
+ - ${placeholder}
356
505
  - Boundaries:
357
- - Specs own stable current facts; project-owned CONTEXT.md and AGENTS.md retain their navigation, context, and instruction roles
506
+ - ${placeholder}
358
507
  - Affected areas:
359
508
  - .rsp/specs/design.md
360
- - Project-owned CONTEXT.md or AGENTS.md when applicable
509
+ - CONTEXT.md
510
+ - AGENTS.md
361
511
  - Durable outcome targets:
362
- - Current facts: .rsp/specs/design.md, existing scoped Specs, CONTEXT.md, or AGENTS.md as applicable
363
- - Lasting rationale: one Decision Record under the configured authoritative path, only when a hard-to-reverse tradeoff exists
512
+ - Current facts: ${placeholder}
513
+ - Lasting rationale: ${placeholder}
364
514
  - Constraints:
365
- - Keep the setup lightweight and avoid duplicating durable project facts
515
+ - ${placeholder}
366
516
 
367
- ## Tasks
368
- - [ ] Review the repository structure, entrypoints, and primary outputs
369
- - [ ] Review project-owned CONTEXT.md and AGENTS.md and confirm the RSP entry points to the right project files
370
- - [ ] Fill .rsp/specs/design.md with durable architecture facts
371
- - [ ] Add stable project-owned context, instructions, or validation steps only where the Host Project convention requires them
517
+ ${changeSectionHeading("tasks")}
518
+ - [ ] .rsp/specs/design.md: ${placeholder}
519
+ - [ ] CONTEXT.md: ${placeholder}
520
+ - [ ] AGENTS.md: ${placeholder}
372
521
 
373
- ## Verify
522
+ ${changeSectionHeading("verify")}
374
523
  - Automated:
375
- - [ ] Run rsp doctor \u2014 proves: the initialized RSP workspace is structurally valid
524
+ - [ ] rsp doctor \u2014 proves: ${placeholder}
376
525
  - Manual or environment:
377
- - [ ] Review .rsp/specs/design.md and project-owned context or instructions and confirm they match the repository
526
+ - [ ] ${placeholder}
378
527
  - Coverage:
379
- - none
528
+ - ${placeholder}
380
529
 
381
- ## Blockers
530
+ ${changeSectionHeading("blockers")}
382
531
  - none
383
532
  `;
384
533
  }
@@ -390,9 +539,9 @@ kind: ops
390
539
  kind: "${frontmatterKind}"
391
540
  ---
392
541
 
393
- # Change: ${name}
542
+ ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
394
543
 
395
- ## Proposal
544
+ ${changeSectionHeading("proposal")}
396
545
  - Outcome: ${proposalSummary}
397
546
  - Why:
398
547
  - ${template.why}
@@ -401,199 +550,117 @@ kind: "${frontmatterKind}"
401
550
  - Non-goals:
402
551
  - ${template.nonGoals}
403
552
 
404
- ## Spec
553
+ ${changeSectionHeading("spec")}
405
554
  ${template.specSection}
406
555
 
407
556
  ### Acceptance
408
557
  ${template.acceptanceSection}
409
558
 
410
- ## Design
559
+ ${changeSectionHeading("design")}
411
560
  - Approach:
412
561
  - ${template.approach}
413
562
  - Boundaries:
414
- - <module, interface, data, document, or ownership boundary affected>
563
+ - ${placeholder}
415
564
  - Affected areas:
416
565
  - ${template.affectedArea1}
417
566
  - ${template.affectedArea2}
418
567
  - Constraints:
419
568
  - ${template.constraints}
420
569
 
421
- ## Tasks
570
+ ${changeSectionHeading("tasks")}
422
571
  - [ ] ${template.task}
423
572
 
424
- ## Verify
573
+ ${changeSectionHeading("verify")}
425
574
  - Automated:
426
- - [ ] ${template.automatedVerify} \u2014 proves: <behavior or scope covered>
575
+ - [ ] ${template.automatedVerify} \u2014 proves: ${placeholder}
427
576
  - Manual or environment:
428
577
  - [ ] ${template.manualVerify}
429
578
  - Coverage:
430
- - <omitted or unavailable relevant coverage and reason, or none>
579
+ - ${placeholder}
431
580
 
432
- ## Blockers
581
+ ${changeSectionHeading("blockers")}
433
582
  - none
434
583
  `;
435
584
  }
436
585
  function generateLiteChangeContent(name, proposalSummary, frontmatterKind) {
586
+ const placeholder = "<\u2026>";
437
587
  return `---
438
588
  kind: "${frontmatterKind}"
439
589
  ---
440
590
 
441
- # Change: ${name}
591
+ ${renderDocumentTitle(CHANGE_DOCUMENT_SCHEMA, name)}
442
592
 
443
- ## Proposal
593
+ ${changeSectionHeading("proposal")}
444
594
  - Outcome: ${proposalSummary}
445
595
  - Why:
446
- - <why this small change matters>
596
+ - ${placeholder}
447
597
  - Scope:
448
- - <what will change>
598
+ - ${placeholder}
449
599
  - Non-goals:
450
600
  - none
451
601
 
452
- ## Spec
602
+ ${changeSectionHeading("spec")}
453
603
  ### MODIFIED
454
- - Requirement: <observable outcome>
455
- - <what should be true after this change>
604
+ - Requirement: ${placeholder}
605
+ - ${placeholder}
456
606
 
457
607
  ### Acceptance
458
- #### Scenario: change is complete
459
- - GIVEN <current context>
460
- - WHEN <the change is applied>
461
- - THEN <expected outcome>
608
+ #### Scenario: ${placeholder}
609
+ - GIVEN ${placeholder}
610
+ - WHEN ${placeholder}
611
+ - THEN ${placeholder}
462
612
 
463
- ## Design
613
+ ${changeSectionHeading("design")}
464
614
  - Approach:
465
- - <minimal implementation approach>
615
+ - ${placeholder}
466
616
  - Boundaries:
467
- - <primary ownership or interface boundary>
617
+ - ${placeholder}
468
618
  - Affected areas:
469
- - <primary file, directory, or doc path>
619
+ - ${placeholder}
470
620
  - Constraints:
471
- - <important constraint, or none>
621
+ - ${placeholder}
472
622
 
473
- ## Tasks
474
- - [ ] Implement the small change
623
+ ${changeSectionHeading("tasks")}
624
+ - [ ] ${placeholder}
475
625
 
476
- ## Verify
626
+ ${changeSectionHeading("verify")}
477
627
  - Automated:
478
- - [ ] <cheapest decisive check; retain a new test only when justified, or not applicable: reason> \u2014 proves: <behavior or scope covered>
628
+ - [ ] ${placeholder} \u2014 proves: ${placeholder}
479
629
  - Manual or environment:
480
- - [ ] <exact acceptance scenario, or unavailable/not applicable: owner and reason>
630
+ - [ ] ${placeholder}
481
631
  - Coverage:
482
- - <omitted or unavailable relevant coverage and reason, or none>
632
+ - ${placeholder}
483
633
 
484
- ## Blockers
634
+ ${changeSectionHeading("blockers")}
485
635
  - none
486
636
  `;
487
637
  }
488
638
  function getChangeTemplateByKind(kind) {
489
- const automatedVerify = "<cheapest decisive check; retain a new test only when justified, or not applicable: reason>";
490
- switch (kind) {
491
- case "feature":
492
- return {
493
- why: "<what user need or capability gap this addresses>",
494
- scope: "<what new behavior or capability will be delivered>",
495
- nonGoals: "<what related capabilities are explicitly out of scope>",
496
- specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new user-facing behavior>\n - <verifiable requirement for the new capability>",
497
- acceptanceSection: "#### Scenario: user exercises the new capability\n- GIVEN <context>\n- WHEN <user action>\n- THEN <expected new behavior>",
498
- approach: "<how the new capability will be implemented>",
499
- affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
500
- affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
501
- constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
502
- task: "<implement the new behavior and its acceptance scenario in the affected areas>",
503
- automatedVerify,
504
- manualVerify: "<exact end-to-end user scenario to validate the new capability, or unavailable/not applicable: owner and reason>"
505
- };
506
- case "fix":
507
- return {
508
- why: "<what current behavior is wrong or broken>",
509
- scope: "<what specific defect is being corrected>",
510
- nonGoals: "<what related but separate issues are out of scope>",
511
- specSection: "<!-- Describe expected correct behavior. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: correct behavior\n - <expected correct behavior after the fix>",
512
- acceptanceSection: "#### Scenario: defect is resolved\n- GIVEN <conditions that trigger the defect>\n- WHEN <action that previously failed>\n- THEN <expected correct outcome>\n- AND the original broken outcome no longer occurs",
513
- approach: "<confirmed cause and correction strategy, or exact evidence needed to establish the cause>",
514
- affectedArea1: "<concrete file path, directory, module, or subsystem containing the defect>",
515
- affectedArea2: "<secondary path, dependency, or test file if needed>",
516
- constraints: "<regression, backward-compatibility, safety, or scope constraint that must hold>",
517
- task: "<identify the root cause and apply the minimal fix>",
518
- automatedVerify,
519
- manualVerify: "<exact steps to reproduce the original issue and confirm it no longer occurs, or unavailable/not applicable: owner and reason>"
520
- };
521
- case "refactor":
522
- return {
523
- why: "<what maintainability, readability, or structural issue motivates this>",
524
- scope: "<what code areas are being restructured>",
525
- nonGoals: "<no behavior change is expected or intended>",
526
- specSection: "<!-- Describe the desired structural outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: internal structure improvement\n - <desired structural outcome without observable behavior change>",
527
- acceptanceSection: "#### Scenario: behavior is preserved after restructuring\n- GIVEN the existing codebase before the refactor\n- WHEN the refactored code runs against existing tests\n- THEN all existing tests pass unchanged",
528
- approach: "<how the code will be restructured (rename, extract, move, etc.)>",
529
- affectedArea1: "<concrete file path, directory, module, or subsystem being refactored>",
530
- affectedArea2: "<secondary path, dependency, or test file if needed>",
531
- constraints: "<observable behavior must remain unchanged; compatibility, performance, and safety constraints must still hold>",
532
- task: "<restructure the code while keeping behavior identical>",
533
- automatedVerify,
534
- manualVerify: "<exact scenario to spot-check that behavior is unchanged after the refactor, or unavailable/not applicable: owner and reason>"
535
- };
536
- case "docs":
537
- return {
538
- why: "<why the documentation change matters>",
539
- scope: "<which docs, readers, or workflows will be updated>",
540
- nonGoals: "<what documentation behavior or audience is out of scope>",
541
- specSection: "<!-- Describe what the reader should see or experience. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: documentation accuracy\n - <what durable reader-facing behavior or explanation changes>",
542
- acceptanceSection: "#### Scenario: reader follows the updated guidance\n- GIVEN the updated documentation\n- WHEN a reader follows the documented workflow\n- THEN the steps are accurate and sufficient",
543
- approach: "<how the documentation will be updated and organized>",
544
- affectedArea1: "<concrete doc path, directory, module doc, or documentation surface 1>",
545
- affectedArea2: "<secondary doc path or reference file if needed>",
546
- constraints: "<durable wording, consistency, scope, or audience constraint that must hold>",
547
- task: "<update the target documentation and supporting references>",
548
- automatedVerify,
549
- manualVerify: "<exact reader or operator scenario to confirm the updated guidance is accurate, or unavailable/not applicable: owner and reason>"
550
- };
551
- case "research":
552
- return {
553
- why: "<what question or uncertainty this research resolves>",
554
- scope: "<what system area, option set, or hypothesis is being examined>",
555
- nonGoals: "<what implementation work is explicitly out of scope>",
556
- specSection: "<!-- Describe what finding or decision must be captured. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: research outcome recording\n - <what finding, option, or decision must be captured clearly>",
557
- acceptanceSection: "#### Scenario: research question is resolved\n- GIVEN the current uncertainty or open question\n- WHEN the investigation is completed\n- THEN the result is recorded clearly enough to guide follow-up work",
558
- approach: "<how the investigation will be performed and what evidence will be gathered>",
559
- affectedArea1: "<concrete file path, directory, module, subsystem, or source under investigation>",
560
- affectedArea2: "<secondary path, dependency, environment, or evidence source if needed>",
561
- constraints: "<time, evidence, safety, or scope constraint for the investigation>",
562
- task: "<gather evidence, record the recommendation or finding, and note any follow-up implementation work if needed>",
563
- automatedVerify,
564
- manualVerify: "<exact review of the findings to confirm they answer the original question and identify follow-up work if needed, or unavailable/not applicable: owner and reason>"
565
- };
566
- case "ops":
567
- return {
568
- why: "<why this operational or environment change matters>",
569
- scope: "<what environment, workflow, or operational path changes>",
570
- nonGoals: "<what product or feature behavior will not change>",
571
- specSection: "<!-- Describe the reliable operational outcome. Implementation notes belong in ## Design. -->\n### MODIFIED\n- Requirement: operational behavior\n - <what reliable operational outcome should change or stay true>",
572
- acceptanceSection: "#### Scenario: operational path succeeds\n- GIVEN the target environment or workflow\n- WHEN the operational change is applied\n- THEN the expected operational outcome is reliable",
573
- approach: "<how the operational change will be applied safely>",
574
- affectedArea1: "<concrete script, config path, workflow, environment path, or operational surface 1>",
575
- affectedArea2: "<secondary script, config path, dependency, or environment surface if needed>",
576
- constraints: "<rollback, safety, reliability, environment, or scope constraint that must hold>",
577
- task: "<apply and verify the operational change>",
578
- automatedVerify,
579
- manualVerify: "<exact operator workflow or environment scenario to confirm the operational result, or unavailable/not applicable: owner and reason>"
580
- };
581
- default:
582
- return {
583
- why: "<why this change matters>",
584
- scope: "<what this change will do>",
585
- nonGoals: "<what this change will not do>",
586
- specSection: "<!-- Describe observable behavior and requirements. Implementation notes belong in ## Design. -->\n### ADDED\n- Requirement: <new or updated behavior>\n - <verifiable requirement>",
587
- acceptanceSection: "#### Scenario: <name>\n- GIVEN <context>\n- WHEN <action>\n- THEN <expected outcome>",
588
- approach: "<how the change will be implemented>",
589
- affectedArea1: "<concrete file path, directory, module, or subsystem 1>",
590
- affectedArea2: "<concrete file path, directory, module, or subsystem 2 if needed>",
591
- constraints: "<behavior, compatibility, performance, safety, or scope constraint that must hold>",
592
- task: "<implement the core change in the affected areas>",
593
- automatedVerify,
594
- manualVerify: "<exact acceptance scenario, or unavailable/not applicable: owner and reason>"
595
- };
596
- }
639
+ const placeholder = "<\u2026>";
640
+ const delta = kind === "feature" || kind === "research" || kind === void 0 ? "ADDED" : "MODIFIED";
641
+ return {
642
+ why: placeholder,
643
+ scope: placeholder,
644
+ nonGoals: placeholder,
645
+ specSection: `<!-- ${placeholder} -->
646
+ ### ${delta}
647
+ - Requirement: ${placeholder}
648
+ - ${placeholder}`,
649
+ acceptanceSection: `#### Scenario: ${placeholder}
650
+ - GIVEN ${placeholder}
651
+ - WHEN ${placeholder}
652
+ - THEN ${placeholder}`,
653
+ approach: placeholder,
654
+ affectedArea1: placeholder,
655
+ affectedArea2: placeholder,
656
+ constraints: placeholder,
657
+ task: placeholder,
658
+ automatedVerify: placeholder,
659
+ manualVerify: placeholder
660
+ };
661
+ }
662
+ function changeSectionHeading(sectionId) {
663
+ return renderDocumentSectionHeading(CHANGE_DOCUMENT_SCHEMA, sectionId);
597
664
  }
598
665
  function renderRspAgentsBlock() {
599
666
  return `${RSP_AGENTS_BEGIN}
@@ -675,78 +742,73 @@ async function detectProjectName() {
675
742
  return basename(process.cwd());
676
743
  }
677
744
  function generateDesignContent(projectName) {
745
+ const placeholder = "<\u2026>";
678
746
  return `# Project Design: ${projectName}
679
747
 
680
748
  ## Purpose
681
- - <what this project is responsible for>
682
- - <who or what it serves>
749
+ - ${placeholder}
683
750
 
684
751
  ## Stable Facts
685
- - <durable fact that future agents or developers must know>
686
- - <key architectural invariant>
752
+ - ${placeholder}
687
753
 
688
754
  ## Boundaries
689
755
  - In scope:
690
- - <major capability or boundary>
756
+ - ${placeholder}
691
757
  - Out of scope:
692
- - <intentional non-goal>
758
+ - ${placeholder}
693
759
 
694
760
  ## Structure
695
- - <important directory or subsystem> \u2014 <responsibility>
761
+ - ${placeholder}
696
762
 
697
763
  ## Constraints
698
- - <cross-cutting technical or operational constraint, when it affects architecture>
764
+ - ${placeholder}
699
765
  `;
700
766
  }
701
767
  function generateSpecContent(name) {
702
768
  const title = toTitleCase(name);
769
+ const placeholder = "<\u2026>";
703
770
  return `# ${title}
704
771
 
705
772
  ## Purpose
706
- - <why this project-level spec exists>
773
+ - ${placeholder}
707
774
 
708
775
  ## Stable Facts
709
- - <durable fact that future work must know>
776
+ - ${placeholder}
710
777
 
711
778
  ## Boundaries
712
779
  - In scope:
713
- - <what this spec covers>
780
+ - ${placeholder}
714
781
  - Out of scope:
715
- - <what this spec does not cover>
782
+ - ${placeholder}
716
783
 
717
784
  ## Constraints
718
- - <stable constraint, if any>
785
+ - ${placeholder}
719
786
  `;
720
787
  }
721
788
  function toTitleCase(value) {
722
789
  return value.split(/[-/]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
723
790
  }
724
- function extractSection(content, heading) {
725
- const lines = content.split("\n");
726
- const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
727
- if (start === -1)
728
- return "";
729
- const body = [];
730
- for (let index = start + 1; index < lines.length; index++) {
731
- const line = lines[index];
732
- if (line.startsWith("## "))
733
- break;
734
- body.push(line);
735
- }
736
- return body.join("\n").trim();
737
- }
738
791
  function extractBlockerLines(content) {
739
- const uncommented = content.replace(/<!--[\s\S]*?-->/g, "");
740
- return extractSection(uncommented, "Blockers").split("\n").map((line) => line.trim()).filter(Boolean);
792
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
793
+ return normalizeBlockerBody(getDocumentSectionBody(document, "blockers"));
794
+ }
795
+ function hasMeaningfulBlockerBody(body) {
796
+ return hasMeaningfulBlockerLines(normalizeBlockerBody(body));
797
+ }
798
+ function normalizeBlockerBody(body) {
799
+ return body.replace(/<!--[\s\S]*?-->/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
741
800
  }
742
801
  function hasMeaningfulBlockers(content) {
743
- const lines = extractBlockerLines(content);
802
+ return hasMeaningfulBlockerLines(extractBlockerLines(content));
803
+ }
804
+ function hasMeaningfulBlockerLines(lines) {
744
805
  if (lines.length === 0)
745
806
  return false;
746
807
  return lines.some((line) => !/^[-*]\s*(?:none)?$/i.test(line) && !/^none$/i.test(line));
747
808
  }
748
809
  function detectDeltaSections(content) {
749
- const body = extractSection(content, "Spec");
810
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
811
+ const body = getDocumentSectionBody(document, "spec");
750
812
  return {
751
813
  added: /^###\s*ADDED/im.test(body),
752
814
  modified: /^###\s*MODIFIED/im.test(body),
@@ -755,8 +817,9 @@ function detectDeltaSections(content) {
755
817
  }
756
818
  function collectArchiveReadiness(content, options = {}) {
757
819
  const warnings = [];
758
- const tasksSection = extractSection(content, "Tasks");
759
- const verifySection = extractSection(content, "Verify");
820
+ const document = parseRspDocument(content, CHANGE_DOCUMENT_SCHEMA);
821
+ const tasksSection = getDocumentSectionBody(document, "tasks");
822
+ const verifySection = getDocumentSectionBody(document, "verify");
760
823
  const taskTodos = getOpenCheckboxes(tasksSection);
761
824
  if (taskTodos.length > 0)
762
825
  warnings.push(`${taskTodos.length} task item(s) still incomplete`);
@@ -835,7 +898,7 @@ var ARCHIVES_DIR = join4(RSP_DIR, "archives");
835
898
  var LOCK_PATH = join4(RSP_DIR, ".lock");
836
899
  var CONFIG_PATH = join4(RSP_DIR, "config.yaml");
837
900
  var VALID_KINDS = ["feature", "fix", "refactor", "docs", "ops", "research"];
838
- var DEFAULT_REQUIRED_SECTIONS = ["Proposal", "Spec", "Design", "Tasks", "Verify", "Blockers"];
901
+ var DEFAULT_REQUIRED_SECTIONS = getCanonicalSectionHeadings(CHANGE_DOCUMENT_SCHEMA);
839
902
  var DEFAULT_MANAGE_POLICY = { activation: "explicit", closeout: "local" };
840
903
  var FAILED_CLOSED_MANAGE_POLICY = { activation: "explicit", closeout: "manual" };
841
904
  var MANAGE_ACTIVATIONS = ["explicit", "auto"];
@@ -865,6 +928,7 @@ async function inspectRspConfig() {
865
928
  const decisions = parsed.decisions && typeof parsed.decisions === "object" && !Array.isArray(parsed.decisions) ? parsed.decisions : void 0;
866
929
  const decisionPath = decisions?.path;
867
930
  const manageValue = parsed.manage && typeof parsed.manage === "object" && !Array.isArray(parsed.manage) ? parsed.manage : void 0;
931
+ const languageValue = parsed.language && typeof parsed.language === "object" && !Array.isArray(parsed.language) ? parsed.language : void 0;
868
932
  const activation = MANAGE_ACTIVATIONS.includes(manageValue?.activation) ? manageValue?.activation : void 0;
869
933
  const closeout = MANAGE_CLOSEOUTS.includes(manageValue?.closeout) ? manageValue?.closeout : void 0;
870
934
  const kinds = Array.isArray(parsed.kinds) && parsed.kinds.every((value) => typeof value === "string" && value.trim() !== "") ? parsed.kinds.map((value) => value.trim()) : void 0;
@@ -873,7 +937,8 @@ async function inspectRspConfig() {
873
937
  config: {
874
938
  kinds,
875
939
  decisions: decisionRecordsIssue === null && validateDecisionRecordsPath(decisionPath) === null ? { path: normalizeDecisionRecordsPath(String(decisionPath)) } : void 0,
876
- manage: manageValue ? { activation, closeout } : void 0
940
+ manage: manageValue ? { activation, closeout } : void 0,
941
+ language: parseLanguageConfig(languageValue)
877
942
  },
878
943
  issues
879
944
  };
@@ -882,7 +947,7 @@ async function inspectRspConfig() {
882
947
  }
883
948
  function validateRspConfig(parsed) {
884
949
  const issues = [];
885
- const supported = /* @__PURE__ */ new Set(["kinds", "decisions", "manage"]);
950
+ const supported = /* @__PURE__ */ new Set(["kinds", "decisions", "manage", "language"]);
886
951
  for (const key of Object.keys(parsed)) {
887
952
  if (key === "required_sections") {
888
953
  issues.push('config.yaml field "required_sections" is no longer supported');
@@ -921,6 +986,24 @@ function validateRspConfig(parsed) {
921
986
  issues.push('config.yaml field "manage.closeout" must be one of: manual, lifecycle, local');
922
987
  }
923
988
  }
989
+ if ("language" in parsed) {
990
+ if (!parsed.language || typeof parsed.language !== "object" || Array.isArray(parsed.language)) {
991
+ issues.push('config.yaml field "language" must be a YAML mapping');
992
+ } else {
993
+ const language = parsed.language;
994
+ const supportedLanguageFields = /* @__PURE__ */ new Set(["default", "artifacts", "commit"]);
995
+ for (const key of Object.keys(language)) {
996
+ if (!supportedLanguageFields.has(key))
997
+ issues.push(`config.yaml field "language.${key}" is not supported`);
998
+ }
999
+ if (!("default" in language))
1000
+ issues.push('config.yaml field "language.default" is required when language is configured');
1001
+ for (const key of supportedLanguageFields) {
1002
+ if (key in language && canonicalizeLanguageTag(language[key]) === null)
1003
+ issues.push(`config.yaml field "language.${key}" must be a non-empty valid BCP 47 language tag`);
1004
+ }
1005
+ }
1006
+ }
924
1007
  return issues;
925
1008
  }
926
1009
  function clearConfigCache() {
@@ -940,6 +1023,37 @@ function resolveManagePolicy(config, options = {}) {
940
1023
  closeout: config.manage?.closeout ?? DEFAULT_MANAGE_POLICY.closeout
941
1024
  };
942
1025
  }
1026
+ function resolveLanguagePolicy(config, options = {}) {
1027
+ if (options.configValid === false || !config.language)
1028
+ return { artifacts: null, commit: null };
1029
+ return {
1030
+ artifacts: config.language.artifacts ?? config.language.default,
1031
+ commit: config.language.commit ?? config.language.default
1032
+ };
1033
+ }
1034
+ function parseLanguageConfig(value) {
1035
+ if (!value)
1036
+ return void 0;
1037
+ const defaultLanguage = canonicalizeLanguageTag(value.default);
1038
+ if (defaultLanguage === null)
1039
+ return void 0;
1040
+ const artifacts = canonicalizeLanguageTag(value.artifacts);
1041
+ const commit = canonicalizeLanguageTag(value.commit);
1042
+ return {
1043
+ default: defaultLanguage,
1044
+ artifacts: artifacts ?? void 0,
1045
+ commit: commit ?? void 0
1046
+ };
1047
+ }
1048
+ function canonicalizeLanguageTag(value) {
1049
+ if (typeof value !== "string" || value.trim() === "")
1050
+ return null;
1051
+ try {
1052
+ return Intl.getCanonicalLocales(value.trim())[0] ?? null;
1053
+ } catch {
1054
+ return null;
1055
+ }
1056
+ }
943
1057
  async function getVersion() {
944
1058
  const { version } = JSON.parse(await readFile2(join4(PKG_ROOT, "package.json"), "utf-8"));
945
1059
  return version;
@@ -975,6 +1089,19 @@ export {
975
1089
  resolveDecisionRecordsPath,
976
1090
  ensureDecisionRecordsDirectory,
977
1091
  validateDecisionRecordsFilesystemPath,
1092
+ CHANGE_DOCUMENT_SCHEMA,
1093
+ GROUP_BRIEF_DOCUMENT_SCHEMA,
1094
+ DocumentSectionCardinalityError,
1095
+ parseRspDocument,
1096
+ getDocumentTitles,
1097
+ getDocumentTitle,
1098
+ getDocumentSections,
1099
+ getDocumentSectionBody,
1100
+ getDocumentSectionDefinitionByHeading,
1101
+ getCanonicalSectionHeadings,
1102
+ renderDocumentSectionHeading,
1103
+ renderDocumentTitle,
1104
+ appendDocumentSectionItem,
978
1105
  toErrorMessage,
979
1106
  emitJson,
980
1107
  recordRuntimeDiagnostic,
@@ -992,8 +1119,8 @@ export {
992
1119
  detectProjectName,
993
1120
  generateDesignContent,
994
1121
  generateSpecContent,
995
- extractSection,
996
1122
  extractBlockerLines,
1123
+ hasMeaningfulBlockerBody,
997
1124
  hasMeaningfulBlockers,
998
1125
  detectDeltaSections,
999
1126
  collectArchiveReadiness,
@@ -1018,5 +1145,6 @@ export {
1018
1145
  resolveKinds,
1019
1146
  resolveRequiredSections,
1020
1147
  resolveManagePolicy,
1148
+ resolveLanguagePolicy,
1021
1149
  getVersion
1022
1150
  };