@planu/cli 4.4.2 → 4.5.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 (37) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/engine/elicitation/answer-extractor.js +53 -1
  3. package/dist/engine/elicitation/decision-gap-detector.d.ts +3 -0
  4. package/dist/engine/elicitation/decision-gap-detector.js +162 -0
  5. package/dist/engine/elicitation/question-grounding-gate.d.ts +3 -0
  6. package/dist/engine/elicitation/question-grounding-gate.js +54 -0
  7. package/dist/engine/implementation-contract/common.d.ts +5 -0
  8. package/dist/engine/implementation-contract/common.js +30 -0
  9. package/dist/engine/implementation-contract/evaluator.d.ts +3 -0
  10. package/dist/engine/implementation-contract/evaluator.js +105 -0
  11. package/dist/engine/implementation-contract/index.d.ts +4 -0
  12. package/dist/engine/implementation-contract/index.js +4 -0
  13. package/dist/engine/implementation-contract/renderer.d.ts +4 -0
  14. package/dist/engine/implementation-contract/renderer.js +98 -0
  15. package/dist/engine/readiness-checker.js +18 -6
  16. package/dist/engine/skill-registry/index.d.ts +1 -0
  17. package/dist/engine/skill-registry/index.js +1 -0
  18. package/dist/engine/skill-registry/installer.d.ts +2 -2
  19. package/dist/engine/skill-registry/installer.js +69 -37
  20. package/dist/engine/skill-registry/skill-security-scanner.d.ts +12 -0
  21. package/dist/engine/skill-registry/skill-security-scanner.js +87 -0
  22. package/dist/engine/spec-format/bdd-parser.js +9 -0
  23. package/dist/engine/spec-format/lean-spec-generator.js +10 -2
  24. package/dist/tools/challenge-spec/implementation-contract-challenge-scenarios.d.ts +3 -0
  25. package/dist/tools/challenge-spec/implementation-contract-challenge-scenarios.js +51 -0
  26. package/dist/tools/challenge-spec.js +4 -0
  27. package/dist/tools/create-spec/question-generator.d.ts +1 -1
  28. package/dist/tools/create-spec/question-generator.js +20 -96
  29. package/dist/tools/create-spec.js +19 -2
  30. package/dist/tools/skill-registry/install.js +9 -0
  31. package/dist/types/clarification.d.ts +8 -0
  32. package/dist/types/elicitation.d.ts +21 -0
  33. package/dist/types/skill-registry.d.ts +59 -0
  34. package/dist/types/spec-format.d.ts +21 -0
  35. package/package.json +18 -18
  36. package/planu-native.json +29 -8
  37. package/planu-plugin.json +35 -7
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.5.0] - 2026-06-10
2
+
3
+ ### Features
4
+ - feat(create-spec): generate intent-grounded questions
5
+
6
+ ### Chores
7
+ - chore(deps): update release tooling
8
+
9
+
10
+ ## [4.4.3] - 2026-06-09
11
+
12
+ ### Features
13
+ - feat(SPEC-1081): add skill security scan gate
14
+ - feat(SPEC-1082): add implementation contract readiness
15
+
16
+
1
17
  ## [4.4.2] - 2026-06-05
2
18
 
3
19
  ### Bug Fixes
@@ -4035,4 +4051,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4035
4051
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4036
4052
  - Multi-language i18n (EN/ES/PT) for generated specs
4037
4053
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4038
- - 10,857 tests with ≥95% coverage
4054
+ - 10,857 tests with ≥95% coverage
@@ -5,6 +5,12 @@ const BACKEND_TERMS = /\b(api|endpoint|route|handler|migration|query|rpc|action|
5
5
  const INFRA_TERMS = /\b(deploy|ci|cd|docker|pipeline|config|env)\b/;
6
6
  const BILLING_TERMS = /\b(payment|billing|invoice|subscription|checkout|pricing|pay)\b/;
7
7
  const DATABASE_TERMS = /\b(database|schema|migration|table|query|sql|rls|rpc|persist|persistence|data access)\b/;
8
+ const ACTION_RE = /\b(add|build|create|fix|update|remove|delete|integrate|sync|import|export|approve|reject|upload|download|migrate|configure|validate)\b\s+(?:(?:a|an|the|new)\s+)?([a-z0-9][a-z0-9 -]{1,60})/i;
9
+ const ACTOR_TERMS = /\b(admin|administrator|manager|owner|customer|user|member|team|approver)\b/gi;
10
+ const DATA_OBJECT_TERMS = /\b(account|profile|request|invoice|subscription|payment|order|report|file|avatar|table|policy|record|settings)\b/gi;
11
+ const PERMISSION_TERMS = /\b(approval|approve|reject|admin|permission|role|rls|policy|access|auth|login|oauth|owner|manager)\b/;
12
+ const DESTRUCTIVE_TERMS = /\b(delete|remove|archive|discard|drop|truncate|revoke|reset)\b/;
13
+ const RISK_TERMS = /\b(approval|approve|payment|billing|auth|login|permission|role|delete|remove|migration|sync|webhook|external|api|rls|privacy|security)\b/gi;
8
14
  const PAYMENT_PROVIDERS = [
9
15
  { name: 'Stripe', pattern: /\bstripe\b/ },
10
16
  { name: 'PayPal', pattern: /\bpaypal\b/ },
@@ -33,7 +39,34 @@ export function extractSignals(description) {
33
39
  const hasBilling = BILLING_TERMS.test(lower) || namedProvider !== null;
34
40
  const wordCount = description.trim().split(/\s+/).length;
35
41
  const hasScope = wordCount >= 10;
36
- return { hasTarget, hasBilling, hasDatabase, hasUi, namedProvider, hasScope };
42
+ const actionMatch = ACTION_RE.exec(lower);
43
+ const action = actionMatch?.[1] ?? null;
44
+ const domainObject = cleanDomainObject(actionMatch?.[2] ?? null);
45
+ const actors = uniqueMatches(lower, ACTOR_TERMS);
46
+ const dataObjects = uniqueMatches(lower, DATA_OBJECT_TERMS);
47
+ const riskTerms = uniqueMatches(lower, RISK_TERMS);
48
+ const integrations = [
49
+ ...PAYMENT_PROVIDERS.filter(({ pattern }) => pattern.test(lower)).map(({ name }) => name),
50
+ ...extractExternalIntegrations(lower),
51
+ ];
52
+ const hasPermissionRisk = PERMISSION_TERMS.test(lower);
53
+ const hasDestructiveAction = DESTRUCTIVE_TERMS.test(lower);
54
+ return {
55
+ hasTarget,
56
+ hasBilling,
57
+ hasDatabase,
58
+ hasUi,
59
+ namedProvider,
60
+ hasScope,
61
+ action,
62
+ domainObject,
63
+ actors,
64
+ integrations,
65
+ dataObjects,
66
+ hasPermissionRisk,
67
+ hasDestructiveAction,
68
+ riskTerms,
69
+ };
37
70
  }
38
71
  function stripIncidentalReferences(description) {
39
72
  return description
@@ -41,4 +74,23 @@ function stripIncidentalReferences(description) {
41
74
  .replace(/\b\S+\/\S+\b/g, ' ')
42
75
  .replace(/\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|py|go|rs|java|rb|php)\b/g, ' ');
43
76
  }
77
+ function cleanDomainObject(value) {
78
+ if (value === null) {
79
+ return null;
80
+ }
81
+ const cleaned = value
82
+ .replace(/\b(that|with|to|from|when|where|and|or|but|using|in)\b.*$/i, '')
83
+ .replace(/\s+/g, ' ')
84
+ .trim();
85
+ return cleaned.length > 0 ? cleaned : null;
86
+ }
87
+ function uniqueMatches(text, pattern) {
88
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
89
+ const globalPattern = new RegExp(pattern.source, flags);
90
+ return Array.from(new Set(Array.from(text.matchAll(globalPattern)).map((match) => match[0])));
91
+ }
92
+ function extractExternalIntegrations(text) {
93
+ const matches = text.match(/\b([a-z][a-z0-9-]+)\s+(?:api|webhook|sdk|integration)\b/gi) ?? [];
94
+ return Array.from(new Set(matches.map((match) => match.replace(/\s+(api|webhook|sdk|integration)$/i, ''))));
95
+ }
44
96
  //# sourceMappingURL=answer-extractor.js.map
@@ -0,0 +1,3 @@
1
+ import type { DecisionGap, ProjectKnowledge, DescriptionSignals } from '../../types/index.js';
2
+ export declare function detectDecisionGaps(signals: DescriptionSignals, _projectContext: ProjectKnowledge | null): DecisionGap[];
3
+ //# sourceMappingURL=decision-gap-detector.d.ts.map
@@ -0,0 +1,162 @@
1
+ const MAX_GAPS = 3;
2
+ export function detectDecisionGaps(signals, _projectContext) {
3
+ const gaps = [
4
+ buildPermissionGap(signals),
5
+ buildProviderGap(signals),
6
+ buildBillingModelGap(signals),
7
+ buildDataGap(signals),
8
+ buildFailureGap(signals),
9
+ ].filter((gap) => gap !== null);
10
+ if (gaps.length === 0 && !signals.hasScope) {
11
+ gaps.push(buildBehaviorGap(signals));
12
+ }
13
+ return gaps.slice(0, MAX_GAPS);
14
+ }
15
+ function buildPermissionGap(signals) {
16
+ if (!signals.hasPermissionRisk || signals.actors.length > 0) {
17
+ return null;
18
+ }
19
+ const subject = formatSubject(signals);
20
+ return makeGap({
21
+ id: 'permission-owner',
22
+ kind: 'permission',
23
+ header: 'Permission',
24
+ question: `For ${subject}, who is allowed to perform this action? This changes permissions, validation, and audit behavior.`,
25
+ evidence: evidenceFrom(signals, ['permission-sensitive wording']),
26
+ impact: 'Changes authorization checks and acceptance criteria.',
27
+ options: options([
28
+ ['Role-based owner (Recommended)', 'Use the existing role or owner model for this action.'],
29
+ ['Admin only', 'Restrict the behavior to administrators.'],
30
+ ['Any signed-in user', 'Allow all authenticated users and keep checks minimal.'],
31
+ ]),
32
+ });
33
+ }
34
+ function buildProviderGap(signals) {
35
+ if (!signals.hasBilling || signals.namedProvider !== null) {
36
+ return null;
37
+ }
38
+ const subject = formatSubject(signals);
39
+ return makeGap({
40
+ id: 'payment-provider',
41
+ kind: 'provider',
42
+ header: 'Provider',
43
+ question: `For ${subject}, which payment provider should own the payment flow? This changes API contracts, webhook handling, and test fixtures.`,
44
+ evidence: evidenceFrom(signals, ['billing/payment wording']),
45
+ impact: 'Changes integration code, webhook behavior, and verification fixtures.',
46
+ options: options([
47
+ ['Stripe (Recommended)', 'Use the most common provider for subscription and checkout flows.'],
48
+ ['PayPal', 'Use PayPal checkout and payment APIs.'],
49
+ [
50
+ 'No provider yet',
51
+ 'Keep provider integration out of scope and define internal contracts only.',
52
+ ],
53
+ ]),
54
+ });
55
+ }
56
+ function buildBillingModelGap(signals) {
57
+ if (!signals.hasBilling) {
58
+ return null;
59
+ }
60
+ const subject = formatSubject(signals);
61
+ return makeGap({
62
+ id: 'billing-model',
63
+ kind: 'billing-model',
64
+ header: 'Billing',
65
+ question: `For ${subject}, is the billing behavior recurring, one-time, or invoice-based? This changes states, events, and acceptance criteria.`,
66
+ evidence: evidenceFrom(signals, ['billing/payment wording']),
67
+ impact: 'Changes lifecycle states and acceptance criteria.',
68
+ options: options([
69
+ ['Recurring subscription (Recommended)', 'Model ongoing subscriptions and renewal states.'],
70
+ ['One-time checkout', 'Model a single payment completion flow.'],
71
+ ['Invoice-based', 'Model invoice creation, payment, and overdue states.'],
72
+ ]),
73
+ });
74
+ }
75
+ function buildDataGap(signals) {
76
+ if (!signals.hasDatabase || signals.dataObjects.length > 0) {
77
+ return null;
78
+ }
79
+ const subject = formatSubject(signals);
80
+ return makeGap({
81
+ id: 'data-shape',
82
+ kind: 'data',
83
+ header: 'Data',
84
+ question: `For ${subject}, what data must be stored or updated? This changes schema, validation, and migration work.`,
85
+ evidence: evidenceFrom(signals, ['database/data wording']),
86
+ impact: 'Changes schema, persistence, and validation requirements.',
87
+ options: options([
88
+ [
89
+ 'Existing records only (Recommended)',
90
+ 'Use existing tables or records without a new schema.',
91
+ ],
92
+ ['New table or field', 'Add persistence changes and migration coverage.'],
93
+ ['Read-only data', 'Do not persist new state for this behavior.'],
94
+ ]),
95
+ });
96
+ }
97
+ function buildFailureGap(signals) {
98
+ if (signals.integrations.length === 0 &&
99
+ !signals.riskTerms.includes('sync') &&
100
+ !signals.riskTerms.includes('api')) {
101
+ return null;
102
+ }
103
+ const subject = formatSubject(signals);
104
+ return makeGap({
105
+ id: 'failure-handling',
106
+ kind: 'failure',
107
+ header: 'Failure',
108
+ question: `For ${subject}, what should happen if ${formatIntegration(signals)} fails? This changes retry, error, and user-visible behavior.`,
109
+ evidence: evidenceFrom(signals, signals.integrations.length > 0 ? signals.integrations : ['integration/API wording']),
110
+ impact: 'Changes error handling, retries, and tests.',
111
+ options: options([
112
+ ['Show recoverable error (Recommended)', 'Return a clear error and preserve current state.'],
113
+ ['Retry automatically', 'Retry transient failures before surfacing an error.'],
114
+ ['Queue for later', 'Persist work for asynchronous retry.'],
115
+ ]),
116
+ });
117
+ }
118
+ function buildBehaviorGap(signals) {
119
+ const subject = formatSubject(signals);
120
+ return makeGap({
121
+ id: 'behavior-outcome',
122
+ kind: 'behavior',
123
+ header: 'Behavior',
124
+ question: `For ${subject}, what observable outcome should prove the work is done? This becomes the primary acceptance criterion.`,
125
+ evidence: evidenceFrom(signals, ['short request']),
126
+ impact: 'Changes the main acceptance criterion and verification command.',
127
+ options: options([
128
+ ['User-visible workflow (Recommended)', 'Define the end-to-end behavior a user can observe.'],
129
+ ['Backend behavior', 'Define API, persistence, job, or integration behavior.'],
130
+ ['Cleanup or fix', 'Define the before/after bug or cleanup result.'],
131
+ ]),
132
+ });
133
+ }
134
+ function makeGap(gap) {
135
+ return { ...gap, multiSelect: gap.multiSelect ?? false, blocking: true };
136
+ }
137
+ function formatSubject(signals) {
138
+ const action = signals.action ?? 'this request';
139
+ const object = signals.domainObject;
140
+ return object === null ? action : `${action} ${object}`;
141
+ }
142
+ function formatIntegration(signals) {
143
+ if (signals.integrations.length > 0) {
144
+ return signals.integrations[0] ?? 'the integration';
145
+ }
146
+ if (signals.riskTerms.includes('sync')) {
147
+ return 'the sync';
148
+ }
149
+ return 'the API';
150
+ }
151
+ function evidenceFrom(signals, extra) {
152
+ return [
153
+ ...(signals.action ? [`action:${signals.action}`] : []),
154
+ ...(signals.domainObject ? [`object:${signals.domainObject}`] : []),
155
+ ...signals.riskTerms.map((term) => `risk:${term}`),
156
+ ...extra,
157
+ ].slice(0, 6);
158
+ }
159
+ function options(items) {
160
+ return items.map(([label, description]) => ({ label, description }));
161
+ }
162
+ //# sourceMappingURL=decision-gap-detector.js.map
@@ -0,0 +1,3 @@
1
+ import type { InteractiveQuestion, QuestionGroundingContext, QuestionGroundingResult } from '../../types/index.js';
2
+ export declare function validateQuestionGrounding(question: InteractiveQuestion, context: QuestionGroundingContext): QuestionGroundingResult;
3
+ //# sourceMappingURL=question-grounding-gate.d.ts.map
@@ -0,0 +1,54 @@
1
+ const GENERIC_QUESTION_PATTERNS = [
2
+ /^who (are|is) the users\??$/i,
3
+ /^what are the edge cases\??$/i,
4
+ /^what should happen\??$/i,
5
+ /^what is the scope\??$/i,
6
+ /^what data is needed\??$/i,
7
+ /^what are the requirements\??$/i,
8
+ ];
9
+ export function validateQuestionGrounding(question, context) {
10
+ const text = question.question.trim();
11
+ if (GENERIC_QUESTION_PATTERNS.some((pattern) => pattern.test(text))) {
12
+ return { passed: false, reason: 'Question is a generic reusable prompt.' };
13
+ }
14
+ const lowerQuestion = text.toLowerCase();
15
+ const lowerEvidence = [context.requestText, ...context.gap.evidence].join(' ').toLowerCase();
16
+ const evidenceTokens = tokenSet(lowerEvidence);
17
+ const hasRequestEvidence = Array.from(evidenceTokens).some((token) => token.length >= 3 && lowerQuestion.includes(token));
18
+ if (!hasRequestEvidence) {
19
+ return {
20
+ passed: false,
21
+ reason: 'Question does not include evidence from the request or detected gap.',
22
+ };
23
+ }
24
+ if (!/\bchanges?\b|\bthis changes\b|\baffects?\b|\bbecause\b|\bbecomes?\b/i.test(text)) {
25
+ return {
26
+ passed: false,
27
+ reason: 'Question does not explain why the answer matters for the spec.',
28
+ };
29
+ }
30
+ return { passed: true };
31
+ }
32
+ function tokenSet(text) {
33
+ return new Set(text
34
+ .replace(/[^a-z0-9 ]/gi, ' ')
35
+ .split(/\s+/)
36
+ .filter((token) => !STOP_WORDS.has(token) && token.length > 2));
37
+ }
38
+ const STOP_WORDS = new Set([
39
+ 'the',
40
+ 'and',
41
+ 'for',
42
+ 'this',
43
+ 'that',
44
+ 'with',
45
+ 'from',
46
+ 'what',
47
+ 'when',
48
+ 'where',
49
+ 'should',
50
+ 'action',
51
+ 'object',
52
+ 'risk',
53
+ ]);
54
+ //# sourceMappingURL=question-grounding-gate.js.map
@@ -0,0 +1,5 @@
1
+ export declare const IMPLEMENTATION_CONTRACT_SECTION = "Implementation Contract";
2
+ export declare const IMPLEMENTATION_CONTRACT_SUBSECTIONS: readonly ["User Outcome", "Behavior Contract", "File-Level Work Plan", "Acceptance-To-Verification Map", "Edge Cases And Failure Modes", "Non-Goals And Forbidden Approaches", "Verification Commands"];
3
+ export declare function hasImplementationContract(specBody: string): boolean;
4
+ export declare function extractTopLevelSection(body: string, sectionName: string): string;
5
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1,30 @@
1
+ export const IMPLEMENTATION_CONTRACT_SECTION = 'Implementation Contract';
2
+ export const IMPLEMENTATION_CONTRACT_SUBSECTIONS = [
3
+ 'User Outcome',
4
+ 'Behavior Contract',
5
+ 'File-Level Work Plan',
6
+ 'Acceptance-To-Verification Map',
7
+ 'Edge Cases And Failure Modes',
8
+ 'Non-Goals And Forbidden Approaches',
9
+ 'Verification Commands',
10
+ ];
11
+ export function hasImplementationContract(specBody) {
12
+ return extractTopLevelSection(specBody, IMPLEMENTATION_CONTRACT_SECTION).trim().length > 0;
13
+ }
14
+ export function extractTopLevelSection(body, sectionName) {
15
+ const normalized = body.replace(/\r\n/g, '\n');
16
+ const headingRe = new RegExp(`^##\\s+${escapeRegex(sectionName)}[ \\t]*$`, 'm');
17
+ const match = headingRe.exec(normalized);
18
+ if (!match) {
19
+ return '';
20
+ }
21
+ const start = match.index + match[0].length;
22
+ const nextRe = /^##\s+\S/gm;
23
+ nextRe.lastIndex = start;
24
+ const next = nextRe.exec(normalized);
25
+ return normalized.slice(start, next ? next.index : normalized.length).trim();
26
+ }
27
+ function escapeRegex(input) {
28
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ }
30
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,3 @@
1
+ import type { ImplementationContractIssue } from '../../types/index.js';
2
+ export declare function evaluateImplementationContract(specBody: string, acceptanceCriteria: string[]): ImplementationContractIssue[];
3
+ //# sourceMappingURL=evaluator.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { extractTopLevelSection, IMPLEMENTATION_CONTRACT_SECTION, IMPLEMENTATION_CONTRACT_SUBSECTIONS, } from './common.js';
2
+ const FILLER_PATTERNS = [
3
+ /\bTBD\b/i,
4
+ /\bN\/A\b/i,
5
+ /\bimplement correctly\b/i,
6
+ /\bhandle errors\b/i,
7
+ /\bto be determined\b/i,
8
+ ];
9
+ export function evaluateImplementationContract(specBody, acceptanceCriteria) {
10
+ const contract = extractTopLevelSection(specBody, IMPLEMENTATION_CONTRACT_SECTION);
11
+ if (contract.trim().length === 0) {
12
+ return [
13
+ {
14
+ code: 'contract_missing',
15
+ message: 'Implementation Contract is missing — add user outcome, behavior, file plan, verification map, edge cases, non-goals, and commands.',
16
+ },
17
+ ];
18
+ }
19
+ const issues = [];
20
+ const sections = new Map(IMPLEMENTATION_CONTRACT_SUBSECTIONS.map((section) => [
21
+ section,
22
+ extractSubsection(contract, section),
23
+ ]));
24
+ for (const [section, content] of sections.entries()) {
25
+ if (content === null) {
26
+ issues.push({
27
+ code: 'contract_subsection_missing',
28
+ message: `Implementation Contract subsection "${section}" is missing.`,
29
+ });
30
+ continue;
31
+ }
32
+ const trimmed = content.trim();
33
+ if (trimmed.length === 0) {
34
+ issues.push({
35
+ code: 'contract_subsection_empty',
36
+ message: `Implementation Contract subsection "${section}" is empty.`,
37
+ });
38
+ }
39
+ for (const pattern of FILLER_PATTERNS) {
40
+ if (pattern.test(trimmed)) {
41
+ issues.push({
42
+ code: 'contract_filler',
43
+ message: `Implementation Contract subsection "${section}" contains filler text.`,
44
+ });
45
+ break;
46
+ }
47
+ }
48
+ if (/\bNeeds decision:/i.test(trimmed)) {
49
+ issues.push({
50
+ code: 'contract_needs_decision',
51
+ message: `Implementation Contract subsection "${section}" still has unresolved Needs decision items.`,
52
+ });
53
+ }
54
+ }
55
+ const map = sections.get('Acceptance-To-Verification Map') ?? '';
56
+ for (const criterion of acceptanceCriteria) {
57
+ if (!criterionHasVerification(criterion, map)) {
58
+ issues.push({
59
+ code: 'contract_unmapped_criterion',
60
+ message: `Acceptance criterion is not mapped to verification evidence: ${criterion}`,
61
+ });
62
+ }
63
+ }
64
+ if (/\bmanual verification\b/i.test(map) &&
65
+ !/\b(given|when|then|step|observe|click|run)\b/i.test(map)) {
66
+ issues.push({
67
+ code: 'contract_manual_verification_vague',
68
+ message: 'Manual verification in the Implementation Contract must include exact observable steps.',
69
+ });
70
+ }
71
+ return issues;
72
+ }
73
+ function extractSubsection(sectionBody, subsectionName) {
74
+ const headingRe = new RegExp(`^###\\s+${escapeRegex(subsectionName)}[ \\t]*$`, 'm');
75
+ const match = headingRe.exec(sectionBody);
76
+ if (!match) {
77
+ return null;
78
+ }
79
+ const start = match.index + match[0].length;
80
+ const nextRe = /^###\s+\S/gm;
81
+ nextRe.lastIndex = start;
82
+ const next = nextRe.exec(sectionBody);
83
+ return sectionBody.slice(start, next ? next.index : sectionBody.length);
84
+ }
85
+ function criterionHasVerification(criterion, map) {
86
+ const normalizedCriterion = normalize(criterion);
87
+ if (normalizedCriterion.length === 0) {
88
+ return true;
89
+ }
90
+ const short = normalizedCriterion.slice(0, 48);
91
+ const normalizedMap = normalize(map);
92
+ return (normalizedMap.includes(short) &&
93
+ /\b(unit test|integration test|cli\/tool result|typecheck\/lint command|snapshot\/golden fixture|manual verification|pnpm|vitest|test)\b/i.test(map));
94
+ }
95
+ function normalize(value) {
96
+ return value
97
+ .replace(/[`*_()[\]:]/g, ' ')
98
+ .replace(/\s+/g, ' ')
99
+ .trim()
100
+ .toLowerCase();
101
+ }
102
+ function escapeRegex(input) {
103
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ }
105
+ //# sourceMappingURL=evaluator.js.map
@@ -0,0 +1,4 @@
1
+ export * from './common.js';
2
+ export * from './renderer.js';
3
+ export * from './evaluator.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export * from './common.js';
2
+ export * from './renderer.js';
3
+ export * from './evaluator.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import type { ImplementationContractInput } from '../../types/index.js';
2
+ export declare function buildImplementationContractSection(input: ImplementationContractInput): string;
3
+ export declare function appendImplementationContractIfMissing(specBody: string, input: ImplementationContractInput): string;
4
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { hasImplementationContract, IMPLEMENTATION_CONTRACT_SECTION } from './common.js';
2
+ export function buildImplementationContractSection(input) {
3
+ const criteria = input.criteria.length > 0 ? input.criteria : fallbackCriteria();
4
+ const testFiles = input.files.test.map((file) => file.path);
5
+ const verificationCommands = input.verificationCommands.length > 0
6
+ ? input.verificationCommands
7
+ : ['pnpm typecheck', 'pnpm lint', 'pnpm test'];
8
+ const lines = [
9
+ `## ${IMPLEMENTATION_CONTRACT_SECTION}`,
10
+ '### User Outcome',
11
+ firstConcreteSentence(input.description) ??
12
+ 'Needs decision: define the exact user-visible outcome this spec must deliver.',
13
+ '',
14
+ '### Behavior Contract',
15
+ ...renderBehaviorContract(criteria),
16
+ '- Inputs: use the user request and existing project conventions as the source of truth.',
17
+ '- Outputs: update canonical `spec.md` behavior without creating sidecar implementation docs.',
18
+ '- State changes: persist only the files and metadata required by the spec workflow.',
19
+ '- Errors: missing implementation detail must surface as `Needs decision:` instead of generic filler.',
20
+ '',
21
+ '### File-Level Work Plan',
22
+ ...renderFilePlan(input.files),
23
+ '',
24
+ '### Acceptance-To-Verification Map',
25
+ ...criteria.map((criterion, index) => renderVerificationRow(criterion.text, index + 1, testFiles, verificationCommands)),
26
+ '',
27
+ '### Edge Cases And Failure Modes',
28
+ '- Generated specs with too little context must use `Needs decision:` placeholders.',
29
+ '- Existing specs without this section must remain readable and must not be rewritten by readiness checks.',
30
+ '- A heading with filler text must not count as a usable implementation contract.',
31
+ '- Manual verification must include exact observable steps when no automated proof is practical.',
32
+ '',
33
+ '### Non-Goals And Forbidden Approaches',
34
+ ...renderNonGoals(input.outOfScope),
35
+ '',
36
+ '### Verification Commands',
37
+ ...verificationCommands.map((command) => `- \`${command}\``),
38
+ '',
39
+ ];
40
+ return lines.join('\n');
41
+ }
42
+ export function appendImplementationContractIfMissing(specBody, input) {
43
+ if (hasImplementationContract(specBody)) {
44
+ return specBody.endsWith('\n') ? specBody : `${specBody}\n`;
45
+ }
46
+ return `${specBody.trimEnd()}\n\n${buildImplementationContractSection(input)}`;
47
+ }
48
+ function fallbackCriteria() {
49
+ return [
50
+ {
51
+ text: 'Needs decision: define at least one acceptance criterion with executable verification.',
52
+ done: false,
53
+ },
54
+ ];
55
+ }
56
+ function firstConcreteSentence(description) {
57
+ const body = description
58
+ .replace(/^---[\s\S]*?---/, '')
59
+ .replace(/^#{1,6}\s+.+$/gm, '')
60
+ .split('\n')
61
+ .map((line) => line.trim())
62
+ .find((line) => line.length >= 24 && !line.startsWith('-'));
63
+ if (!body) {
64
+ return null;
65
+ }
66
+ return body.replace(/\s+/g, ' ').slice(0, 240);
67
+ }
68
+ function renderFilePlan(files) {
69
+ const lines = [];
70
+ for (const [label, entries] of [
71
+ ['Create', files.create],
72
+ ['Modify', files.modify],
73
+ ['Test', files.test],
74
+ ]) {
75
+ for (const entry of entries) {
76
+ lines.push(`- ${label}: \`${entry.path}\` (${entry.status})`);
77
+ }
78
+ }
79
+ return lines.length > 0 ? lines : ['- Needs decision: identify expected source and test files.'];
80
+ }
81
+ function renderBehaviorContract(criteria) {
82
+ return criteria.map((criterion, index) => `- Expected behavior AC${String(index + 1)}: ${criterion.text}`);
83
+ }
84
+ function renderVerificationRow(criterion, index, testFiles, commands) {
85
+ const test = testFiles[0];
86
+ if (test) {
87
+ return `- AC${String(index)}: ${criterion} -> unit test \`${test}\` plus \`${commands[0] ?? 'pnpm test'}\`.`;
88
+ }
89
+ return `- AC${String(index)}: ${criterion} -> Needs decision: choose unit test, integration test, CLI/tool result, snapshot/golden fixture, or exact manual verification.`;
90
+ }
91
+ function renderNonGoals(outOfScope) {
92
+ const base = [
93
+ '- Do not create durable sidecar implementation docs outside canonical `spec.md`.',
94
+ '- Do not treat headings or filler text as implementation-ready detail.',
95
+ ];
96
+ return outOfScope.length > 0 ? [...base, ...outOfScope.map((item) => `- ${item}`)] : base;
97
+ }
98
+ //# sourceMappingURL=renderer.js.map
@@ -4,7 +4,8 @@ import { readSpecTechnicalSection } from './spec-format/read-technical-section.j
4
4
  import { stripFrontmatter } from './frontmatter-parser.js';
5
5
  import { loadReadinessConfig } from './readiness-config-loader.js';
6
6
  import { runEarsGate } from './ears-gate.js';
7
- import { findScenariosWithoutTests } from './validator/spec-compliance-runner.js';
7
+ import { findScenariosWithoutTests, parseFrontmatterScenarios, } from './validator/spec-compliance-runner.js';
8
+ import { evaluateImplementationContract } from './implementation-contract/index.js';
8
9
  // ── SPEC-784: Technical section quality constants ─────────────────────────────
9
10
  const TECHNICAL_MIN_CHARS = 500;
10
11
  // Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
@@ -110,6 +111,14 @@ export function extractCriteriaLines(huContent) {
110
111
  }
111
112
  return criteria;
112
113
  }
114
+ function frontmatterScenarioCriteria(raw) {
115
+ return parseFrontmatterScenarios(raw).map((scenario) => {
116
+ const tests = scenario.tests
117
+ ?.map((test) => `${test.path}${test.line !== undefined ? ` line ${String(test.line)}` : ''}`)
118
+ .join(' ') ?? '';
119
+ return [scenario.title, tests].filter((part) => part.trim().length > 0).join(' ');
120
+ });
121
+ }
113
122
  function scoreCriteria(criteriaLines, vagueWords) {
114
123
  const blockers = [];
115
124
  const warnings = [];
@@ -397,10 +406,8 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
397
406
  // When a spec uses BDD format, acceptance criteria are stored as `scenarios:` in
398
407
  // the YAML frontmatter (multi-line YAML that the simple KV parser cannot handle).
399
408
  // If no criteria were found in the body, fall back to counting frontmatter scenarios.
400
- const scenarioCount = criteriaLines.length === 0 ? countFrontmatterScenarios(huRaw) : 0;
401
- const effectiveCriteriaLines = scenarioCount > 0
402
- ? Array.from({ length: scenarioCount }).fill('bdd-scenario')
403
- : criteriaLines;
409
+ const scenarioCriteria = criteriaLines.length === 0 ? frontmatterScenarioCriteria(huRaw) : [];
410
+ const effectiveCriteriaLines = scenarioCriteria.length > 0 ? scenarioCriteria : criteriaLines;
404
411
  const hu = scoreHuCompleteness(spec);
405
412
  const criteria = scoreCriteria(effectiveCriteriaLines, vagueWords);
406
413
  const files = scoreFilesIdentified(spec, fichaContent);
@@ -416,6 +423,11 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
416
423
  ...earsBlockers,
417
424
  ];
418
425
  const allWarnings = [...hu.warnings, ...criteria.warnings, ...files.warnings, ...deps.warnings];
426
+ if (mode === 'strict' && spec.scope !== 'trivial' && spec.difficulty >= 3) {
427
+ for (const issue of evaluateImplementationContract(huContent, effectiveCriteriaLines)) {
428
+ allBlockers.push(`${issue.code}: ${issue.message}`);
429
+ }
430
+ }
419
431
  // SPEC-732: Executable AC gate — block approved when any scenario lacks a `tests` array.
420
432
  // Only enforce when the spec uses frontmatter scenarios (BDD format).
421
433
  const bddScenarioCount = countFrontmatterScenarios(huRaw);
@@ -436,7 +448,7 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
436
448
  }
437
449
  // SPEC-629: Specificity gate caps score at 60 for difficulty >= 3 specs that lack
438
450
  // concrete file paths, function names, or anticipated test breaks.
439
- const specificityBlockers = checkSpecificityGate(spec, criteriaLines, fichaContent);
451
+ const specificityBlockers = checkSpecificityGate(spec, effectiveCriteriaLines, fichaContent);
440
452
  allBlockers.push(...specificityBlockers);
441
453
  const effectiveScore = specificityBlockers.length > 0 && spec.difficulty >= 3 ? Math.min(totalScore, 60) : totalScore;
442
454
  const breakdown = {
@@ -1,5 +1,6 @@
1
1
  export { searchAllRegistries } from './unified-search.js';
2
2
  export { installSkill, isSkillInstalled, readSkillManifest } from './installer.js';
3
+ export { scanSkillSecurity } from './skill-security-scanner.js';
3
4
  export { searchAnthropicSkills, fetchAnthropicSkillContent, clearAnthropicCache, } from './anthropic-adapter.js';
4
5
  export { searchSkillsSh } from './skillssh-adapter.js';
5
6
  export { searchAgentSkills } from './agentskill-adapter.js';