Attribute Stories to Source Requirements
You are the lineage attribution agent. You are given one imported source file and the complete list of stories that already exist for this Target. Identify the distinct requirements the source states, and for each one name the stories that implement it.
This is a matching task against a closed set. Every story you may name is listed in <stories>. You are not decomposing work, proposing new stories, or judging whether the existing stories are correct.
Method
- Read the source and identify each distinct requirement it states. A requirement is a thing the
system must do, at whatever granularity the author wrote it. One sentence may state one requirement that several stories implement — for example "add a table and show it on screen" is one requirement implemented by a schema story, a route story, and a view story. Do not split a requirement to make the mapping tidier, and do not merge two requirements that a reader would act on separately.
- Give each requirement a short kebab-case name that describes it. The name is an identifier, not
a summary: mark-book-read, not the-reader-can-mark-a-book-as-read.
- For each requirement, list every story that implements any part of it. A story may implement
more than one requirement; a requirement may need more than one story.
- List any story that implements no requirement in the source as
<unattached>. This is expected
and correct for foundational work — application scaffolding, configuration, shared UI framing, test harnesses — that the author never asked for by name. Do not force such a story onto an unrelated requirement.
Rules
- Use only story ids that appear in
<stories>. Never invent one. - Every story must appear exactly once, either inside a
storiesattribute or as<unattached>. - Quote the requirement text verbatim from the source in the tag body. Do not paraphrase it.
- Emit nothing but the tags below. No preamble, no commentary, no explanation.
Output
<requirement name="add-remove-books" stories="add-book,remove-book,database">
The reader can add a book with a title and author, view the books in the order added, and remove a book.
</requirement>
<requirement name="reject-empty-fields" stories="validate-book">
An empty title or author is rejected with a clear error message.
</requirement>
<unattached story="architecture"/>
<unattached story="ui-general"/>
Attribution job
<source name="normalize.py">
-- coding: utf-8 --
from html.parser import HTMLParser from urllib.parse import quote, unquote
try: from html.parser import HTMLParseError except ImportError: # HTMLParseError was removed in Python 3.5. It could never be # thrown, so we define a placeholder instead. class HTMLParseError(Exception): pass
from html.entities import name2codepoint import sys import re import html
Normalization code, adapted from
https://github.com/karlcow/markdown-testsuite/
significant_attrs = ["alt", "href", "src", "title"] whitespace_re = re.compile(r"\s+") class MyHTMLParser(HTMLParser): def init(self): HTMLParser.init(self) self.convert_charrefs = False self.last = "starttag" self.in_pre = False self.output = "" self.last_tag = "" def handle_data(self, data): after_tag = self.last == "endtag" or self.last == "starttag" after_block_tag = after_tag and self.is_block_tag(self.last_tag) if after_tag and self.last_tag == "br": data = data.lstrip('\n') if not self.in_pre: data = whitespace_re.sub(' ', data) if after_block_tag and not self.in_pre: if self.last == "starttag": data = data.lstrip() elif self.last == "endtag": data = data.strip() self.output += data self.last = "data" def handle_endtag(self, tag): if tag == "pre": self.in_pre = False elif self.is_block_tag(tag): self.output = self.output.rstrip() self.output += "</" + tag + ">" self.last_tag = tag self.last = "endtag" def handle_starttag(self, tag, attrs): if tag == "pre": self.in_pre = True if self.is_block_tag(tag): self.output = self.output.rstrip() self.output += "<" + tag # For now we don't strip out 'extra' attributes, because of # raw HTML test cases. # attrs = filter(lambda attr: attr[0] in significant_attrs, attrs) if attrs: attrs.sort() for (k,v) in attrs: self.output += " " + k if v in ['href','src']: self.output += ("=" + '"' + quote(unquote(v), safe='/') + '"') elif v != None: self.output += ("=" + '"' + html.escape(v,quote=True) + '"') self.output += ">" self.last_tag = tag self.last = "starttag" def handle_startendtag(self, tag, attrs): """Ignore closing tag for self-closing """ self.handle_starttag(tag, attrs) self.last_tag = tag self.last = "endtag" def handle_comment(self, data): self.output += '<!--' + data + '-->' self.last = "comment" def handle_decl(self, data): self.output += '<!' + data + '>' self.last = "decl" def unknown_decl(self, data): self.output += '<!' + data + '>' self.last = "decl" def handle_pi(self,data): self.output += '<?' + data + '>' self.last = "pi" def handle_entityref(self, name): try: c = chr(name2codepoint[name]) except KeyError: c = None self.output_char(c, '&' + name + ';') self.last = "ref" def handle_charref(self, name): try: if name.startswith("x"): c = chr(int(name[1:], 16)) else: c = chr(int(name)) except ValueError: c = None self.output_char(c, '&' + name + ';') self.last = "ref" # Helpers. def output_char(self, c, fallback): if c == '<': self.output += "<" elif c == '>': self.output += ">" elif c == '&': self.output += "&" elif c == '"': self.output += """ elif c == None: self.output += fallback else: self.output += c
def is_block_tag(self,tag): return (tag in ['article', 'header', 'aside', 'hgroup', 'blockquote', 'hr', 'iframe', 'body', 'li', 'map', 'button', 'object', 'canvas', 'ol', 'caption', 'output', 'col', 'p', 'colgroup', 'pre', 'dd', 'progress', 'div', 'section', 'dl', 'table', 'td', 'dt', 'tbody', 'embed', 'textarea', 'fieldset', 'tfoot', 'figcaption', 'th', 'figure', 'thead', 'footer', 'tr', 'form', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'video', 'script', 'style'])
def normalize_html(html): r""" Return normalized form of HTML which ignores insignificant output differences:
Multiple inner whitespaces are collapsed to a single space (except in pre tags):
>>> normalize_html("<p>a \t b</p>") '<p>a b</p>'
>>> normalize_html("<p>a \t\nb</p>") '<p>a b</p>'
- Whitespace surrounding block-level tags is removed.
>>> normalize_html("<p>a b</p>") '<p>a b</p>'
>>> normalize_html(" <p>a b</p>") '<p>a b</p>'
>>> normalize_html("<p>a b</p> ") '<p>a b</p>'
>>> normalize_html("\n\t<p>\n\t\ta b\t\t</p>\n\t") '<p>a b</p>'
>>> normalize_html("<i>a b</i> ") '<i>a b</i> '
- Self-closing tags are converted to open tags.
>>> normalize_html("<br />") '<br>'
- Attributes are sorted and lowercased.
>>> normalize_html('<a title="bar" HREF="foo">x</a>') '<a href="foo" title="bar">x</a>'
- References are converted to unicode, except that '<', '>', '&', and
'"' are rendered using entities.
>>> normalize_html("∀&><"") '\u2200&><"'
""" html_chunk_re = re.compile(r"(\<!\[CDATA\[.?\]\]\>|\<[^>]\>|[^<]+)") try: parser = MyHTMLParser() # We work around HTMLParser's limitations parsing CDATA # by breaking the input into chunks and passing CDATA chunks # through verbatim. for chunk in re.finditer(html_chunk_re, html): if chunk.group(0)[:8] == "<![CDATA": parser.output += chunk.group(0) else: parser.feed(chunk.group(0)) parser.close() return parser.output except HTMLParseError as e: sys.stderr.write("Normalization error: " + e.msg + "\n") return html # on error, return unnormalized HTML </source>
<stories> <story id="architecture" implements="ARCHITECTURE.md">Define parser architecture and module boundaries.</story> <story id="cli-entrypoint" implements="FEATURE-CLI-ENTRYPOINT.md">Implement the standard-input and standard-output parser entry point.</story> <story id="cli-errors" implements="FEATURE-CLI-ERRORS.md">Define deterministic parser process errors and exit behavior.</story> <story id="cli-documentation" implements="FEATURE-CLI-DOCUMENTATION.md">Document parser operation and conformance verification.</story> <story id="block-leaf" implements="FEATURE-BLOCK-LEAF.md">Parse CommonMark leaf blocks.</story> <story id="block-quotes" implements="FEATURE-BLOCK-QUOTES.md">Parse nested block quotes and lazy continuation.</story> <story id="block-lists" implements="FEATURE-BLOCK-LISTS.md">Parse CommonMark list items and list structure.</story> <story id="inline-escapes" implements="FEATURE-INLINE-ESCAPES.md">Parse escapes and character references.</story> <story id="inline-code-breaks" implements="FEATURE-INLINE-CODE-BREAKS.md">Parse code spans and line breaks.</story> <story id="inline-emphasis" implements="FEATURE-INLINE-EMPHASIS.md">Parse emphasis and strong emphasis using delimiter rules.</story> <story id="inline-links" implements="FEATURE-INLINE-LINKS.md">Parse links and images.</story> <story id="inline-html" implements="FEATURE-INLINE-HTML.md">Parse autolinks and raw HTML.</story> <story id="render-blocks" implements="FEATURE-RENDER-BLOCKS.md">Render parsed block structure as CommonMark HTML.</story> <story id="render-inline" implements="FEATURE-RENDER-INLINE.md">Render parsed inline structure as CommonMark HTML.</story> <story id="render-normalization" implements="FEATURE-RENDER-NORMALIZATION.md">Normalize URL, title, attribute, and special-character output.</story> <story id="conformance-assets" implements="FEATURE-CONFORMANCE-ASSETS.md">Stage and exercise the supplied conformance harness within bounded story scopes.</story> <story id="conformance-full" implements="FEATURE-CONFORMANCE-FULL.md">Run the complete supplied CommonMark conformance suite.</story> </stories>