cclaw-cli 0.51.10 → 0.51.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.
@@ -303,26 +303,6 @@ function tokensFromRule(rule) {
303
303
  }
304
304
  return [];
305
305
  }
306
- /**
307
- * Extract required keywords from validation rules that contain *backticked*
308
- * stable tokens after a colon. We only fire on machine-surface enumerations
309
- * (e.g., `` Must contain: `Status:`, `WAIT_FOR_CONFIRM`, `Approved:` ``);
310
- * descriptive English prose with bare comma lists is intentionally ignored so
311
- * authors can write rationale freely without triggering hardcoded keyword
312
- * matches. Sections that need richer structural enforcement use a dedicated
313
- * `validateSectionBody` dispatch (see `validateScopeSummary`, etc.).
314
- */
315
- function extractRequiredKeywords(rule) {
316
- const colonMatch = /:\s*(.+)$/u.exec(rule);
317
- if (!colonMatch)
318
- return [];
319
- const tail = colonMatch[1];
320
- const backtickedTokens = Array.from(tail.matchAll(/`([^`]+)`/gu)).map((m) => m[1].trim());
321
- const phrases = backtickedTokens.filter((p) => p.length >= 2);
322
- if (phrases.length < 3)
323
- return [];
324
- return phrases;
325
- }
326
306
  const VAGUE_AC_ADJECTIVES = [
327
307
  "fast",
328
308
  "quick",
@@ -368,35 +348,6 @@ function getMarkdownTableRows(sectionBody) {
368
348
  }
369
349
  return rows;
370
350
  }
371
- function getApproachRows(sectionBody) {
372
- const tableRows = getMarkdownTableRows(sectionBody).map((row) => row.join(" "));
373
- const headingRows = sectionBody
374
- .split(/\r?\n/u)
375
- .map((line) => line.trim())
376
- .filter((line) => /^#{3,6}\s+\S/u.test(line))
377
- .map((line) => line.replace(/^#{3,6}\s+/u, ""));
378
- const bulletRows = sectionBody
379
- .split(/\r?\n/u)
380
- .map((line) => line.trim())
381
- .filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
382
- return [...tableRows, ...headingRows, ...bulletRows];
383
- }
384
- function hasSemanticChallenger(row) {
385
- const normalized = row
386
- .replace(/[_`*]/gu, " ")
387
- .replace(/\s+/gu, " ")
388
- .trim()
389
- .toLowerCase();
390
- const isChallenger = /\bchallenger\b/u.test(normalized);
391
- if (!isChallenger)
392
- return false;
393
- return (/\bhigher[-\s]?upside\b/u.test(normalized) ||
394
- /\bhigh[-\s]?upside\b/u.test(normalized) ||
395
- /\bupside\s*:?\s*(?:high|higher|strong|large|meaningful)\b/u.test(normalized) ||
396
- /\b(?:high|higher|strong|large|meaningful)\s+upside\b/u.test(normalized) ||
397
- /\b(?:10-star|ten-star|ambitious|higher leverage|leverage)\b/u.test(normalized) ||
398
- /\bhigh\b/u.test(normalized));
399
- }
400
351
  function parseBinaryFlag(value) {
401
352
  const normalized = value.trim().toLowerCase();
402
353
  if (/^(?:y|yes|true|1)$/u.test(normalized))
@@ -550,13 +501,14 @@ function validatePremiseChallenge(sectionBody) {
550
501
  if (rowCount < 3) {
551
502
  return {
552
503
  ok: false,
553
- details: `Premise Challenge needs at least 3 question/answer rows in a table or bullet list (right problem? / direct path? / what if nothing? are the gstack default trio). Found ${rowCount}.`
504
+ details: `Premise Challenge needs at least 3 substantive rows in a table or bullet list. Found ${rowCount}.`
554
505
  };
555
506
  }
556
507
  // For tables, each data row must have at least 2 non-empty cells so the
557
- // section is genuinely a Q/A comparison, not a list of headlines. For
558
- // bullet lists, each line must be substantive (>= 8 characters of letters
559
- // or digits) so we don't accept three-letter placeholders like `- a`.
508
+ // section is genuinely a premise/answer comparison, not a list of headlines.
509
+ // For bullet lists, each line must be substantive so we don't accept
510
+ // placeholders like `- a`; punctuation style and natural language do not
511
+ // matter.
560
512
  if (tableRows.length >= 3) {
561
513
  const sparseRows = tableRows.filter((row) => {
562
514
  const filledCells = row.filter((cell) => cell.replace(/[\s|]/gu, "").length >= 2);
@@ -572,14 +524,13 @@ function validatePremiseChallenge(sectionBody) {
572
524
  else if (bulletRows.length >= 3) {
573
525
  const sparseBullets = bulletRows.filter((line) => {
574
526
  const cleaned = line.replace(/^[-*\d.\s]+/u, "").replace(/[`*_]/gu, "").trim();
575
- const hasQuestionMark = /\?/u.test(cleaned);
576
527
  const meaningful = cleaned.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
577
- return !hasQuestionMark && meaningful < 12;
528
+ return meaningful < 12;
578
529
  });
579
- if (sparseBullets.length > bulletRows.length - 3) {
530
+ if (sparseBullets.length > 0) {
580
531
  return {
581
532
  ok: false,
582
- details: "Premise Challenge bullet list must include at least 3 substantive Q/A lines (a question mark plus the answer, or a labelled `Question: answer` pair)."
533
+ details: "Premise Challenge bullet list must include at least 3 substantive rows, not placeholders."
583
534
  };
584
535
  }
585
536
  }
@@ -616,6 +567,114 @@ function validateScopeSummary(sectionBody) {
616
567
  details: "Scope Summary names the selected mode and the next-stage handoff."
617
568
  };
618
569
  }
570
+ const APPROACH_ROLE_VALUES = ["baseline", "challenger", "wild-card"];
571
+ const APPROACH_UPSIDE_VALUES = ["low", "modest", "high", "higher"];
572
+ const REQUIREMENT_PRIORITY_VALUES = ["P0", "P1", "P2", "P3", "DROPPED"];
573
+ function normalizeTableToken(value) {
574
+ return value
575
+ .replace(/[`*_]/gu, "")
576
+ .trim()
577
+ .toLowerCase()
578
+ .replace(/\s+/gu, "-");
579
+ }
580
+ function columnIndex(header, expected) {
581
+ return header.findIndex((cell) => normalizeTableToken(cell) === expected);
582
+ }
583
+ function validateApproachesTaxonomy(sectionBody) {
584
+ const header = tableHeaderCells(sectionBody);
585
+ const rows = getMarkdownTableRows(sectionBody);
586
+ if (!header) {
587
+ return {
588
+ rowCount: 0,
589
+ roleUpsideOk: false,
590
+ challengerOk: false,
591
+ details: "Approaches must be a markdown table with canonical Role and Upside columns."
592
+ };
593
+ }
594
+ const roleIndex = columnIndex(header, "role");
595
+ const upsideIndex = columnIndex(header, "upside");
596
+ if (roleIndex < 0 || upsideIndex < 0) {
597
+ return {
598
+ rowCount: rows.length,
599
+ roleUpsideOk: false,
600
+ challengerOk: false,
601
+ details: "Approaches table must include canonical `Role` and `Upside` columns (Role: baseline | challenger | wild-card; Upside: low | modest | high | higher)."
602
+ };
603
+ }
604
+ let challengerRows = 0;
605
+ let challengerHasHighUpside = false;
606
+ for (const [index, row] of rows.entries()) {
607
+ const role = normalizeTableToken(row[roleIndex] ?? "");
608
+ const upside = normalizeTableToken(row[upsideIndex] ?? "");
609
+ if (!APPROACH_ROLE_VALUES.includes(role)) {
610
+ return {
611
+ rowCount: rows.length,
612
+ roleUpsideOk: false,
613
+ challengerOk: false,
614
+ details: `Approaches row ${index + 1} has invalid Role "${row[roleIndex] ?? ""}". Expected one of: ${APPROACH_ROLE_VALUES.join(", ")}.`
615
+ };
616
+ }
617
+ if (!APPROACH_UPSIDE_VALUES.includes(upside)) {
618
+ return {
619
+ rowCount: rows.length,
620
+ roleUpsideOk: false,
621
+ challengerOk: false,
622
+ details: `Approaches row ${index + 1} has invalid Upside "${row[upsideIndex] ?? ""}". Expected one of: ${APPROACH_UPSIDE_VALUES.join(", ")}.`
623
+ };
624
+ }
625
+ if (role === "challenger") {
626
+ challengerRows += 1;
627
+ if (upside === "high" || upside === "higher") {
628
+ challengerHasHighUpside = true;
629
+ }
630
+ }
631
+ }
632
+ const challengerOk = challengerRows === 1 && challengerHasHighUpside;
633
+ return {
634
+ rowCount: rows.length,
635
+ roleUpsideOk: true,
636
+ challengerOk,
637
+ details: challengerOk
638
+ ? "Approaches table uses canonical Role/Upside values and exactly one high/higher-upside challenger."
639
+ : `Approaches table must include exactly one challenger row with Upside high or higher. Found ${challengerRows} challenger row(s).`
640
+ };
641
+ }
642
+ function validateRequirementsTaxonomy(sectionBody) {
643
+ const header = tableHeaderCells(sectionBody);
644
+ if (!header) {
645
+ return {
646
+ ok: false,
647
+ details: "Requirements must be a markdown table with a Priority column."
648
+ };
649
+ }
650
+ const priorityIndex = columnIndex(header, "priority");
651
+ if (priorityIndex < 0) {
652
+ return {
653
+ ok: false,
654
+ details: "Requirements table must include a canonical `Priority` column."
655
+ };
656
+ }
657
+ const rows = getMarkdownTableRows(sectionBody);
658
+ if (rows.length === 0) {
659
+ return {
660
+ ok: false,
661
+ details: "Requirements table must include at least one requirement row."
662
+ };
663
+ }
664
+ for (const [index, row] of rows.entries()) {
665
+ const rawPriority = (row[priorityIndex] ?? "").replace(/[`*_]/gu, "").trim().toUpperCase();
666
+ if (!REQUIREMENT_PRIORITY_VALUES.includes(rawPriority)) {
667
+ return {
668
+ ok: false,
669
+ details: `Requirements row ${index + 1} has invalid Priority "${row[priorityIndex] ?? ""}". Expected one of: ${REQUIREMENT_PRIORITY_VALUES.join(", ")}.`
670
+ };
671
+ }
672
+ }
673
+ return {
674
+ ok: true,
675
+ details: "Requirements table uses canonical Priority values."
676
+ };
677
+ }
619
678
  const INTERACTION_EDGE_CASE_REQUIREMENTS = [
620
679
  { label: "double-click", pattern: /\bdouble[\s-]?click\b/iu },
621
680
  {
@@ -1342,6 +1401,9 @@ function validateSectionBody(sectionBody, rule, sectionName) {
1342
1401
  if (sectionNameNormalized === "premise challenge") {
1343
1402
  return validatePremiseChallenge(sectionBody);
1344
1403
  }
1404
+ if (sectionNameNormalized === "requirements") {
1405
+ return validateRequirementsTaxonomy(sectionBody);
1406
+ }
1345
1407
  if (sectionNameNormalized === "data flow") {
1346
1408
  return validateInteractionEdgeCaseMatrix(sectionBody);
1347
1409
  }
@@ -1379,21 +1441,6 @@ function validateSectionBody(sectionBody, rule, sectionName) {
1379
1441
  };
1380
1442
  }
1381
1443
  }
1382
- if (sectionNameNormalized !== "architecture diagram") {
1383
- const keywords = extractRequiredKeywords(rule);
1384
- if (keywords.length > 0) {
1385
- const bodyLower = sectionBody.toLowerCase();
1386
- const found = keywords.filter((kw) => bodyLower.includes(kw.toLowerCase()));
1387
- const threshold = Math.ceil(keywords.length * 0.5);
1388
- if (found.length < threshold) {
1389
- const missing = keywords.filter((kw) => !bodyLower.includes(kw.toLowerCase()));
1390
- return {
1391
- ok: false,
1392
- details: `Rule expects keywords (${threshold}/${keywords.length} minimum): missing ${missing.join(", ")}.`
1393
- };
1394
- }
1395
- }
1396
- }
1397
1444
  if (sectionNameNormalized === "acceptance criteria" &&
1398
1445
  /observable[\s,]*measurable[\s,]+(and )?falsifiable/iu.test(rule)) {
1399
1446
  const rows = getMarkdownTableRows(sectionBody);
@@ -1568,31 +1615,29 @@ export async function lintArtifact(projectRoot, stage, track = "standard") {
1568
1615
  }
1569
1616
  const approachesBody = sectionBodyByName(sections, "Approaches");
1570
1617
  if (approachesBody !== null) {
1571
- const tableRows = getMarkdownTableRows(approachesBody);
1572
- const bulletRows = approachesBody
1573
- .split(/\r?\n/u)
1574
- .map((line) => line.trim())
1575
- .filter((line) => /^(?:[-*]|\d+\.)\s+\S/u.test(line));
1576
- const rowCount = Math.max(tableRows.length, bulletRows.length);
1577
- const approachRows = getApproachRows(approachesBody);
1578
- const hasChallenger = approachRows.some(hasSemanticChallenger);
1618
+ const approachesTaxonomy = validateApproachesTaxonomy(approachesBody);
1579
1619
  findings.push({
1580
1620
  section: "Distinct Approaches Enforcement",
1581
1621
  required: true,
1582
1622
  rule: "Approaches section must document at least 2 distinct approaches so the Iron Law comparison is meaningful.",
1583
- found: rowCount >= 2,
1584
- details: rowCount >= 2
1585
- ? `Detected ${rowCount} approach row(s).`
1586
- : `Detected ${rowCount} approach row(s); at least 2 required.`
1623
+ found: approachesTaxonomy.rowCount >= 2,
1624
+ details: approachesTaxonomy.rowCount >= 2
1625
+ ? `Detected ${approachesTaxonomy.rowCount} approach row(s).`
1626
+ : `Detected ${approachesTaxonomy.rowCount} approach row(s); at least 2 required.`
1627
+ });
1628
+ findings.push({
1629
+ section: "Approaches Role/Upside Taxonomy",
1630
+ required: true,
1631
+ rule: "Approaches table must use canonical Role and Upside enum values.",
1632
+ found: approachesTaxonomy.roleUpsideOk,
1633
+ details: approachesTaxonomy.details
1587
1634
  });
1588
1635
  findings.push({
1589
1636
  section: "Challenger Alternative Enforcement",
1590
1637
  required: true,
1591
1638
  rule: "Approaches must include one challenger option with explicit high/higher upside.",
1592
- found: hasChallenger,
1593
- details: hasChallenger
1594
- ? "Semantic challenger with high/higher upside detected."
1595
- : "Missing a challenger option with explicit high/higher upside. Example: `| C | challenger | high upside | More ambitious path with clear trade-offs |`."
1639
+ found: approachesTaxonomy.challengerOk,
1640
+ details: approachesTaxonomy.details
1596
1641
  });
1597
1642
  }
1598
1643
  const reactionIndex = headingLineIndex(raw, "Approach Reaction");
@@ -31,11 +31,11 @@ const STAGE_EXAMPLES = {
31
31
 
32
32
  ## Approaches
33
33
 
34
- | Approach | Role | Architecture | Trade-offs | Recommendation |
35
- | --- | --- | --- | --- | --- |
36
- | A: Reusable validation module | baseline | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
37
- | B: Hardened shell scripts | fallback | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
38
- | C: Full release framework | challenger: higher-upside | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
34
+ | Approach | Role | Upside | Architecture | Trade-offs | Recommendation |
35
+ | --- | --- | --- | --- | --- | --- |
36
+ | A: Reusable validation module | baseline | high | Shared TS module with typed validators, imported by CI scripts and local CLI. Existing \`pre-publish.sh\` calls the module. | Medium upfront effort, high reuse. Requires test coverage for the module. | **Recommended** — best balance of reuse and delivery speed. |
37
+ | B: Hardened shell scripts | baseline | modest | Keep existing script approach, add stricter checks and error messages. | Lowest effort. Weak reuse, CI/local divergence risk grows over time. | Viable fallback if TS module is blocked. |
38
+ | C: Full release framework | challenger | higher | New release orchestrator with plugin system, config files, rollback commands. | Maximum flexibility. High risk, delivery delay, over-engineered for current needs. | Not recommended for v1. |
39
39
 
40
40
  ## Approach Reaction
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.10",
3
+ "version": "0.51.12",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {