@maintainabilityai/research-runner 0.1.34 → 0.1.36

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.
@@ -685,6 +685,130 @@ function makeSelfReviewHandler(persona) {
685
685
  }
686
686
  const handleSelfReviewArchitect = makeSelfReviewHandler('architect');
687
687
  const handleSelfReviewSecurity = makeSelfReviewHandler('security');
688
+ // ─────────────────────────────────────────────────────────────────────
689
+ // knowledge-prd — D-PR1.v1.1 fix. Was deployed as a Skill template but
690
+ // the runner had no handler, so the code-design-agent's first attempt at
691
+ // invoking it on PR #120 returned `{"ok":false,"reason":"unknown-skill"}`.
692
+ // Agent fell back to direct file read + grep, which worked, but the chain
693
+ // has no `knowledge-prd` event proving the PRD was structurally read.
694
+ //
695
+ // Parses `okrs/<id>/how/prd.md` for FR-NN + SR-NN entries with tolerant
696
+ // regex (mirrors B31's tolerance — accepts `FR-NN` / `FR NN` / `**FR-NN**`
697
+ // heading or bold markers). Best-effort extraction of cited sources +
698
+ // STRIDE / OWASP anchors per requirement.
699
+ // ─────────────────────────────────────────────────────────────────────
700
+ const KnowledgePrdInput = zod_1.z.object({ okrId: zod_1.z.string().min(1) });
701
+ /**
702
+ * Extract FR-NN / SR-NN requirement entries from a PRD body. Tolerant
703
+ * to several markdown forms the prd-agent has emitted over time:
704
+ * - `### FR-01: <title>` (H3 heading)
705
+ * - `**FR-01**: <title>` (bold-anchor inline)
706
+ * - `- **FR-01**: <title>` (bullet w/ bold anchor)
707
+ *
708
+ * Returns one record per logical id. Same id seen twice (heading + bullet
709
+ * form) is deduped — first occurrence wins (heading usually).
710
+ */
711
+ function parsePrdRequirements(body, prefix) {
712
+ const seen = new Set();
713
+ const out = [];
714
+ // Match the requirement id and the rest of the line. The id form
715
+ // accepts `FR-NN` / `FR NN` (no dash) for forgiveness — same shape as
716
+ // B31's `[CRSE]-?\d+`. Captures the text content that follows.
717
+ const idRegex = new RegExp(`(?:^|\\s|\\*\\*)${prefix}[-\\s]?(\\d+)(?:\\*\\*)?\\s*[:.]?\\s*(.*?)(?:\\*\\*|$)`, 'gmi');
718
+ const lines = body.split('\n');
719
+ // Walk line-by-line and accumulate a window of context (this line +
720
+ // next ~6 lines) so source/anchor citations on a "Traces to:" line
721
+ // immediately following the heading get associated with the right id.
722
+ for (let i = 0; i < lines.length; i++) {
723
+ const line = lines[i];
724
+ const m = line.match(new RegExp(`(?:^|\\s|\\*\\*)${prefix}[-\\s]?(\\d+)(?:\\*\\*)?\\s*[:.]\\s*(.*?)\\s*$`, 'i'));
725
+ if (!m) {
726
+ continue;
727
+ }
728
+ const num = m[1];
729
+ const id = `${prefix}-${num.padStart(2, '0')}`;
730
+ if (seen.has(id)) {
731
+ continue;
732
+ }
733
+ seen.add(id);
734
+ let text = (m[2] || '').replace(/\*\*/g, '').trim();
735
+ // Collect 6 lines forward for source / anchor scanning.
736
+ const window = lines.slice(i, Math.min(i + 7, lines.length)).join('\n');
737
+ const sources = [...window.matchAll(/[CRSE]-?\d+/g)].map(x => x[0].replace(/(?<=[CRSE])\B/, '-').replace('--', '-'));
738
+ const dedupSrc = Array.from(new Set(sources));
739
+ const record = { id, text };
740
+ if (prefix === 'FR' && dedupSrc.length > 0) {
741
+ record.sources = dedupSrc;
742
+ }
743
+ if (prefix === 'SR') {
744
+ const stride = [...window.matchAll(/THR-\d{3}/gi)].map(x => x[0].toUpperCase());
745
+ const owasp = [...window.matchAll(/A0[1-9]|A10/gi)].map(x => x[0].toUpperCase());
746
+ if (stride.length > 0) {
747
+ record.stride = Array.from(new Set(stride));
748
+ }
749
+ if (owasp.length > 0) {
750
+ record.owasp = Array.from(new Set(owasp));
751
+ }
752
+ }
753
+ out.push(record);
754
+ }
755
+ void idRegex;
756
+ return out;
757
+ }
758
+ /**
759
+ * Extract a Coverage Analysis table from the PRD body. Format expected:
760
+ * | FR/SR | Source | Status |
761
+ * |---|---|---|
762
+ * | FR-01 | R-2,E-1 | YES |
763
+ * ...
764
+ * Returns a map id → bool (YES → true, PARTIAL/NO → false).
765
+ */
766
+ function parsePrdCoverage(body) {
767
+ const coverage = {};
768
+ const lines = body.split('\n');
769
+ for (const line of lines) {
770
+ const m = line.match(/^\s*\|\s*((?:FR|SR)[-\s]?\d+)\s*\|.*\|\s*(YES|PARTIAL|NO)\s*\|/i);
771
+ if (!m) {
772
+ continue;
773
+ }
774
+ const rawId = m[1].toUpperCase();
775
+ const numMatch = rawId.match(/(\d+)/);
776
+ if (!numMatch) {
777
+ continue;
778
+ }
779
+ const id = `${rawId.startsWith('FR') ? 'FR' : 'SR'}-${numMatch[1].padStart(2, '0')}`;
780
+ coverage[id] = m[2].toUpperCase() === 'YES';
781
+ }
782
+ return coverage;
783
+ }
784
+ const handleKnowledgePrd = async (input) => {
785
+ const parsed = KnowledgePrdInput.safeParse(input);
786
+ if (!parsed.success) {
787
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
788
+ }
789
+ const docPath = path.join(meshPath(), 'okrs', parsed.data.okrId, 'how', 'prd.md');
790
+ if (!fs.existsSync(docPath)) {
791
+ return { ok: false, reason: 'prd-not-merged-yet' };
792
+ }
793
+ const body = fs.readFileSync(docPath, 'utf8');
794
+ const functionalRequirements = parsePrdRequirements(body, 'FR');
795
+ const securityRequirements = parsePrdRequirements(body, 'SR');
796
+ const coverage = parsePrdCoverage(body);
797
+ const auditMetadata = {
798
+ okr_id: parsed.data.okrId,
799
+ fr_count: functionalRequirements.length,
800
+ sr_count: securityRequirements.length,
801
+ coverage_rows: Object.keys(coverage).length,
802
+ };
803
+ return {
804
+ ok: true,
805
+ functionalRequirements,
806
+ securityRequirements,
807
+ coverage,
808
+ docPath,
809
+ auditMetadata,
810
+ };
811
+ };
688
812
  /**
689
813
  * D-PR1 — code-phase persona-switch self-review. Same B29 pattern as the
690
814
  * PRD-phase architect/security handlers above, but reads the WHAT-phase
@@ -1623,6 +1747,10 @@ exports.SKILLS = {
1623
1747
  'knowledge-mesh-threats': handleKnowledgeMeshThreats,
1624
1748
  'knowledge-mesh-adrs': handleKnowledgeMeshAdrs,
1625
1749
  'knowledge-research': handleKnowledgeResearch,
1750
+ // D-PR1.v1.1 — knowledge-prd handler (SKILL.md was deployed but no
1751
+ // runner backend existed, causing the code-design-agent to fall back
1752
+ // to direct file read with no chain evidence on PR #120).
1753
+ 'knowledge-prd': handleKnowledgePrd,
1626
1754
  'context-architecture': handleContextArchitecture,
1627
1755
  'context-security': handleContextSecurity,
1628
1756
  'context-quality': handleContextQuality,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainabilityai/research-runner",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
5
5
  "license": "MIT",
6
6
  "author": "MaintainabilityAI",