initial import of a sphinx based documentation generator.

This commit is contained in:
Samuel
2012-09-28 14:13:51 -04:00
parent b399374c53
commit c5f41ccc42
10 changed files with 564 additions and 3 deletions

1
doc/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_build

View File

@@ -41,11 +41,14 @@ apidocs:
python mkapidoc.py
clean:
rm -Rf api handbook
rm -Rf api handbook html
rm -f *.aux *.log *.out *.pdf *.toc figures/*.png
sphinx:
cd ..; ./version.sh
sphinx-build . html
cd ..; ./version.sh --reset
publish: handbook apidocs
ssh root@debain.org "mkdir -p $(PUBLISH_PATH)"
rsync -azr handbook/* api/* $(PUBLISH_HOST):$(PUBLISH_PATH)/
ssh $(PUBLISH_HOST) "chown -R www-data:www-data $(PUBLISH_PATH)/"
make clean

85
doc/_templates/index.html vendored Normal file
View File

@@ -0,0 +1,85 @@
{% extends "layout.html" %}
{% set title = 'Overview' %}
{% block body %}
<h1>Welcome</h1>
<div class="quotebar">
<p>What users say:</p>
<p>&ldquo;Cheers for a great tool that actually makes programmers <b>want</b>
to write documentation!&rdquo;</p>
</div>
<p>
Sphinx is a tool that makes it easy to create intelligent and beautiful
documentation, written by Georg Brandl and licensed under the BSD license.</p>
<p>It was originally created to translate <a href="http://docs.python.org/dev/">the
new Python documentation</a>, and it has excellent support for the documentation
of Python projects, but other documents can be written with it too. Of course,
this site is also created from reStructuredText sources using Sphinx!
</p>
<p>
It is still under constant development, and the following features are
already present, work fine and can be seen &#8220;in action&#8221; in the
Python docs:
</p>
<ul>
<li><b>Output formats:</b> HTML (including Windows HTML Help) and LaTeX, for
printable PDF versions</li>
<li><b>Extensive cross-references:</b> semantic markup and automatic links
for functions, classes, glossary terms and similar pieces of information</li>
<li><b>Hierarchical structure:</b> easy definition of a document tree, with
automatic links to siblings, parents and children</li>
<li><b>Automatic indices:</b> general index as well as a module index</li>
<li><b>Code handling:</b> automatic highlighting using the <a
href="http://pygments.org">Pygments</a> highlighter</li>
<li><b>Extensions:</b> automatic testing of code snippets, inclusion of
docstrings from Python modules, and more</li>
</ul>
<p>
Sphinx uses <a href="http://docutils.sf.net/rst.html">reStructuredText</a>
as its markup language, and many of its strengths come from the power and
straightforwardness of reStructuredText and its parsing and translating
suite, the <a href="http://docutils.sf.net/">Docutils</a>.
</p>
<h2>Examples</h2>
<p>
The <a href="http://docs.python.org/dev/">Python documentation</a> and
this page are different examples of Sphinx in use.
You can also download a <a href="http://sphinx.pocoo.org/sphinx.pdf">PDF version</a>
of the Sphinx documentation, generated from the LaTeX Sphinx produces.
</p>
<p>
For examples of how Sphinx source files look, use the &#8220;Show source&#8221;
links on all pages of the documentation apart from this welcome page.
</p>
<p>Links to more documentation generated with Sphinx can be found on the
<a href="{{ pathto("examples") }}">Projects using Sphinx</a> page.
</p>
<h2>Documentation</h2>
<table class="contentstable" align="center" style="margin-left: 30px"><tr>
<td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">Contents</a><br/>
<span class="linkdescr">for a complete overview</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("search") }}">Search page</a><br/>
<span class="linkdescr">search the documentation</span></p>
</td><td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("genindex") }}">General Index</a><br/>
<span class="linkdescr">all functions, classes, terms</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("modindex") }}">Module Index</a><br/>
<span class="linkdescr">quick access to all documented modules</span></p>
</td></tr>
</table>
<h2>Get Sphinx</h2>
<p>
Sphinx is available as an <a
href="http://peak.telecommunity.com/DevCenter/EasyInstall">easy-install</a>able
package on the <a href="http://pypi.python.org/pypi/Sphinx">Python Package
Index</a>.
</p>
<p>The code can be found in a Mercurial repository, at
<tt>http://bitbucket.org/birkenfeld/sphinx/</tt>.</p>
{% endblock %}

27
doc/_templates/indexsidebar.html vendored Normal file
View File

@@ -0,0 +1,27 @@
<h3>Download</h3>
{% if version.endswith('(hg)') %}
<p>This documentation is for version <b>{{ version }}</b>, which is
not released yet.</p>
<p>You can use it from the
<a href="http://bitbucket.org/birkenfeld/sphinx/">Mercurial repo</a> or look for
released versions in the <a href="http://pypi.python.org/pypi/Sphinx">Python
Package Index</a>.</p>
{% else %}
<p>Current version: <b>{{ version }}</b></p>
<p>Get Sphinx from the <a href="http://pypi.python.org/pypi/Sphinx">Python Package
Index</a>, or install it with:</p>
<pre>easy_install -U Sphinx</pre>
{% endif %}
<h3>Questions? Suggestions?</h3>
<p>Join the <a href="http://groups.google.com/group/sphinx-dev">Google group</a>:</p>
<form action="http://groups.google.com/group/sphinx-dev/boxsubscribe"
style="padding-left: 1em">
<input type="text" name="email" value="your@email"/>
<input type="submit" name="sub" value="Subscribe" />
</form>
<p>or come to the <tt>#python-docs</tt> channel on FreeNode.</p>
<p>You can also open an issue at the
<a href="http://www.bitbucket.org/birkenfeld/sphinx/issues/">tracker</a>.</p>

17
doc/_templates/layout.html vendored Normal file
View File

@@ -0,0 +1,17 @@
{% extends "!layout.html" %}
{% block rootrellink %}
<li><a href="{{ pathto('index') }}">Sphinx home </a> |&nbsp;</li>
<li><a href="{{ pathto('contents') }}">Documentation </a> &raquo;</li>
{% endblock %}
{% block relbar1 %}
<div style="background-color: white; text-align: left; padding: 10px 10px 15px 15px">
<img src="{{ pathto("_static/sphinx.png", 1) }}" alt="Sphinx logo" />
</div>
{{ super() }}
{% endblock %}
{# put the sidebar before the body #}
{% block sidebar1 %}{{ sidebar() }}{% endblock %}
{% block sidebar2 %}{% endblock %}

188
doc/conf.py Normal file
View File

@@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
#
# Sphinx documentation build configuration file, created by
# sphinx-quickstart.py on Sat Mar 8 21:47:50 2008.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# The contents of this file are pickled, so don't put values in the namespace
# that aren't pickleable (module imports are okay, they're removed automatically).
#
# All configuration values have a default value; values that are commented out
# serve to show the default value.
import sys, os, re
# If your extensions are in another directory, add it here.
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
# General configuration
# ---------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.addons.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General substitutions.
project = 'SpiffWorkflow'
copyright = '2012 ' + ', '.join(open('../AUTHORS').readlines())
# The default replacements for |version| and |release|, also used in various
# other places throughout the built documents.
#
# The short X.Y version.
import SpiffWorkflow
version = SpiffWorkflow.__version__
# The full version, including alpha/beta/rc tags.
release = version
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
show_authors = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'friendly'
# Options for HTML output
# -----------------------
# The style sheet to use for HTML and HTML Help pages. A file of that name
# must exist either in Sphinx' static/ path, or in one of the custom paths
# given in html_static_path.
html_style = 'sphinxdoc.css'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Content template for the index page.
html_index = 'index.html'
# Custom sidebar templates, maps page names to templates.
html_sidebars = {'index': 'indexsidebar.html'}
# Additional templates that should be rendered to pages, maps page names to
# templates.
html_additional_pages = {'index': 'index.html'}
# If true, the reST sources are included in the HTML build as _sources/<name>.
#html_copy_source = True
html_use_opensearch = 'http://sphinx.pocoo.org'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Sphinxdoc'
# Options for LaTeX output
# ------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, document class [howto/manual]).
latex_documents = [('contents', 'sphinx.tex', 'Sphinx Documentation',
'Georg Brandl', 'manual', 1)]
latex_logo = '_static/sphinx.png'
#latex_use_parts = True
# Additional stuff for the LaTeX preamble.
latex_elements = {
'fontpkg': '\\usepackage{palatino}'
}
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# Extension interface
# -------------------
from sphinx import addnodes
dir_sig_re = re.compile(r'\.\. ([^:]+)::(.*)$')
def parse_directive(env, sig, signode):
if not sig.startswith('.'):
dec_sig = '.. %s::' % sig
signode += addnodes.desc_name(dec_sig, dec_sig)
return sig
m = dir_sig_re.match(sig)
if not m:
signode += addnodes.desc_name(sig, sig)
return sig
name, args = m.groups()
dec_name = '.. %s::' % name
signode += addnodes.desc_name(dec_name, dec_name)
signode += addnodes.desc_addname(args, args)
return name
def parse_role(env, sig, signode):
signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig)
return sig
event_sig_re = re.compile(r'([a-zA-Z-]+)\s*\((.*)\)')
def parse_event(env, sig, signode):
m = event_sig_re.match(sig)
if not m:
signode += addnodes.desc_name(sig, sig)
return sig
name, args = m.groups()
signode += addnodes.desc_name(name, name)
plist = addnodes.desc_parameterlist()
for arg in args.split(','):
arg = arg.strip()
plist += addnodes.desc_parameter(arg, arg)
signode += plist
return name
def setup(app):
from sphinx.ext.autodoc import cut_lines
app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
app.add_description_unit('directive', 'dir', 'pair: %s; directive', parse_directive)
app.add_description_unit('role', 'role', 'pair: %s; role', parse_role)
app.add_description_unit('confval', 'confval', 'pair: %s; configuration value')
app.add_description_unit('event', 'event', 'pair: %s; event', parse_event)

79
doc/features.rst Normal file
View File

@@ -0,0 +1,79 @@
.. _features:
Features
========
Supported Workflow Patterns
---------------------------
.. HINT::
All examples are located `here<https://github.com/knipknap/SpiffWorkflow/blob/master/tests/SpiffWorkflow/data/spiff/>`.
Control-Flow Patterns
^^^^^^^^^^^^^^^^^^^^^
1. Sequence [control-flow/sequence.xml]
2. Parallel Split [control-flow/parallel_split.xml]
3. Synchronization [control-flow/synchronization.xml]
4. Exclusive Choice [control-flow/exclusive_choice.xml]
5. Simple Merge [control-flow/simple_merge.xml]
6. Multi-Choice [control-flow/multi_choice.xml]
7. Structured Synchronizing Merge [control-flow/structured_synchronizing_merge.xml]
8. Multi-Merge [control-flow/multi_merge.xml]
9. Structured Discriminator [control-flow/structured_discriminator.xml]
10. Arbitrary Cycles [control-flow/arbitrary_cycles.xml]
11. Implicit Termination [control-flow/implicit_termination.xml]
12. Multiple Instances without Synchronization [control-flow/multi_instance_without_synch.xml]
13. Multiple Instances with a Priori Design-Time Knowledge [control-flow/multi_instance_with_a_priori_design_time_knowledge.xml]
14. Multiple Instances with a Priori Run-Time Knowledge [control-flow/multi_instance_with_a_priori_run_time_knowledge.xml]
15. Multiple Instances without a Priori Run-Time Knowledge [control-flow/multi_instance_without_a_priori.xml]
16. Deferred Choice [control-flow/deferred_choice.xml]
17. Interleaved Parallel Routing [control-flow/interleaved_parallel_routing.xml]
18. Milestone [control-flow/milestone.xml]
19. Cancel Task [control-flow/cancel_task.xml]
20. Cancel Case [control-flow/cancel_case.xml]
21. *NOT IMPLEMENTED*
22. Recursion [control-flow/recursion.xml]
23. Transient Trigger [control-flow/transient_trigger.xml]
24. Persistent Trigger [control-flow/persistent_trigger.xml]
25. Cancel Region [control-flow/cancel_region.xml]
26. Cancel Multiple Instance Task [control-flow/cancel_multi_instance_task.xml]
27. Complete Multiple Instance Task [control-flow/complete_multiple_instance_activity.xml]
28. Blocking Discriminator [control-flow/blocking_discriminator.xml]
29. Cancelling Discriminator [control-flow/cancelling_discriminator.xml]
30. Structured Partial Join [control-flow/structured_partial_join.xml]
31. Blocking Partial Join [control-flow/blocking_partial_join.xml]
32. Cancelling Partial Join [control-flow/cancelling_partial_join.xml]
33. Generalized AND-Join [control-flow/generalized_and_join.xml]
34. Static Partial Join for Multiple Instances [control-flow/static_partial_join_for_multi_instance.xml]
35. Cancelling Partial Join for Multiple Instances [control-flow/cancelling_partial_join_for_multi_instance.xml]
36. Dynamic Partial Join for Multiple Instances [control-flow/dynamic_partial_join_for_multi_instance.xml]
37. Acyclic Synchronizing Merge [control-flow/acyclic_synchronizing_merge.xml]
38. General Synchronizing Merge [control-flow/general_synchronizing_merge.xml]
39. Critical Section [control-flow/critical_section.xml]
40. Interleaved Routing [control-flow/interleaved_routing.xml]
41. Thread Merge [control-flow/thread_merge.xml]
42. Thread Split [control-flow/thread_split.xml]
43. Explicit Termination [control-flow/explicit_termination.xml]
Workflow Data Patterns
^^^^^^^^^^^^^^^^^^^^^^
1. Task Data [data/task_data.xml]
2. Block Data [data/block_data.xml]
3. *NOT IMPLEMENTED*
4. *NOT IMPLEMENTED*
5. *NOT IMPLEMENTED*
6. *NOT IMPLEMENTED*
7. *NOT IMPLEMENTED*
8. *NOT IMPLEMENTED*
9. Task to Task [data/task_to_task.xml]
10. Block Task to Sub-Workflow Decomposition [data/block_to_subworkflow.xml]
11. Sub-Workflow Decomposition to Block Task [data/subworkflow_to_block.xml]
Specs that have no corresponding workflow pattern on workflowpatterns.com
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Execute - spawns a subprocess and waits for the results
- Transform - executes commands that can be used for data transforms
- Celery - executes a Celery task (see http://celeryproject.org/)

6
doc/index.rst Normal file
View File

@@ -0,0 +1,6 @@
.. toctree::
:maxdepth: 2
intro
features
internals

20
doc/internals.rst Normal file
View File

@@ -0,0 +1,20 @@
Internal Details for SpiffWorkflow
==================================
A `derivation tree` is created based off of the spec using a hierarchy of Task objects (not TaskSpecs,
but each Task points to the TaskSpec that generated it).
Think of a derivation tree as tree of execution paths (some, but not all, of which will end up executing). Each Task object is basically a node in the derivation tree. Each task in the tree links back to its parent (there are no connection objects). The processing is done by walking down the derivation tree one Task at a time and moving the task (and it's children) through the sequence of states towards completion.
You can serialize/deserialize specs and open standards like OpenWFE are supported (and others can be coded in easily). You can also serialize/deserialize a running workflow (it will pull in its spec as
well).
Another important distinction is between properties and attributes. Properties belong to TaskSpecs. They are static at run-time and belong to the design of the workflow. Attributes are dynamic and assigned to
Tasks (nodes in the execution path).
There's a decent eventing model that allows you to tie in to and receive events (for each task, you can get event notifications from its TaskSpec). The events correspond with how the processing is going in the derivation tree, not necessarily how the workflow as a whole is moving.
See `TaskSpec.py<https://github.com/knipknap/SpiffWorkflow/blob/master/SpiffWorkflow/specs/TaskSpec.py>` for docs on events.
You can nest workflows (using the SubWorkflowSpec).
The serialization code is done well which makes it easy to add new formats if we need to support them.

135
doc/intro.rst Normal file
View File

@@ -0,0 +1,135 @@
About Spiff Workflow
====================
Spiff Workflow is a workflow engine implemented in pure Python.
It is based on the excellent work of the `Workflow Patterns initiative<http://www.workflowpatterns.com/>`.
It's main design goals are the following:
- Directly support as many of the patterns of workflowpatterns.com as possible.
- Map those patterns into workflow elements that are easy to understand by a user in a workflow GUI editor.
- Provide a clean Python API.
You can find a list of supported workflow patterns in :ref:`features`.
General Concept
---------------
The process of using Spiff Workflow involves the following steps:
# Write a workflow specification. A specification may be written using XML (`example<https://github.com/knipknap/SpiffWorkflow/blob/master/tests/SpiffWorkflow/data/spiff/workflow1.xml>`), JSON, or Python (`example<https://github.com/knipknap/SpiffWorkflow/blob/master/tests/SpiffWorkflow/data/spiff/workflow1.py>`).
# Run the workflow using the Python API. Example code for running the workflow::
from SpiffWorkflow.specs import *
from SpiffWorkflow import Workflow
spec = WorkflowSpec()
# (Add tasks to the spec here.)
wf = Workflow(spec)
wf.complete_task_from_id(...)
Specification vs. Workflow Instance
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
One critical concept to know about SpiffWorkflow is the difference between a `TaskSpec `and `Task` and the difference between a `WorkflowSpec` and `Workflow`.
In order to understand how to handle a running workflow consider the following process::
Choose product -> Choose amount -> Produce product A
`--> Product product B
As you can see, in this case the process resembles a simple tree. *Choose product*, *Choose amount*, *Produce product A*, and *Produce product B* are all specific kinds of *task specifications*, and the whole process is a *workflow specification*.
But when you execute the workflow, the path taken does not necessarily have the same shape. For example, if the user chooses to produce 3 items of product A, the path taken looks like the following::
Choose product -> Choose amount -> Produce product A
|--> Produce product A
`--> Produce product A
This is the reason why you will find two different categories of objects in Spiff Workflow:
- **Specification objects** (WorkflowSpec and TaskSpec) represent the workflow definition, and
- **derivation tree objects** (Workflow and Task) model the task tree that represents the state of a running workflow.
Defining a Workflow
^^^^^^^^^^^^^^^^^^^
The WorkflowSpec and TaskSpec classes are used to define a workflow. SpiffWorkflow has many types of TaskSpecs: Join, Split, Execute, Wait, and all others are derived from TaskSpec. The specs can be serialized and deserialized to a variety of formats.
A WorkflowSpec is built by chaining TaskSpecs together in a tree. You can either assemble workflow using Python objects (see the example linked above), or by loading it from XML such as follows::
from SpiffWorkflow.storage import XmlSerializer
serializer = XmlSerializer()
xml_file = 'my_workflow.xml'
xml_data = open(xml_file).read()
spec = serializer.deserialize_workflow_spec(xml_data, xml_file)
...
(Passing the filename to the deserializer is optional, but improves error messages.)
For a full list of all TaskSpecs see the `SpiffWorkflow.specs<https://github.com/knipknap/SpiffWorkflow/tree/master/SpiffWorkflow/specs>` module. All classes have full API documentation. To understand better how each individual subtype of TaskSpec works, look at `the workflow patterns<http://www.workflowpatterns.com>` web site; especially the flash animations showing how each type of task works.
*Note: The TaskSpec classes named "ThreadXXXX" create logical threads based on the model in http://www.workflowpatterns.com. There is no Python threading implemented.*
Running a workflow
^^^^^^^^^^^^^^^^^^
To run the workflow, create an instance of the *Workflow* class as follows::
from SpiffWorkflow import Workflow
spec = ... # see above
wf = Workflow(spec)
...
The *Workflow* object then represents the state of this particular instance of the running workflow. In other words, it includes the derivation tree and the data, by holding a tree that is composed of *Task* objects.
All changes in the progress or state of a workflow are always reflected in one (or more) of the *Task* objects. Each Task has a *state*, and can hold *data*.
.. HINT::
To visualize the state of a running workflow, you may use the `Workflow.dump()` method to print the task tree to stdout.
Some tasks change their state automatically based on internal or environmental changes. Other tasks may need to be triggered by you, the user. The latter kind of tasks can, for example, be completed by calling::
wf.complete_task_from_id(...)
Understanding task states
^^^^^^^^^^^^^^^^^^^^^^^^^
The following task states exist:
.. image:: figures/state-diagram.png
The states are reached in a strict order and the lines in the diagram show the possible state transitions.
The order of these state transitions is violated only in one case: A *Trigger* task may add additional work to a task that was already COMPLETED, causing it to change the state back to FUTURE.
- **MAYBE** means that the task will possibly, but not necessarily run at a future time. This means that it can not yet be fully determined as to whether or not it may run, for example, because the execution still depends on the outcome of an ExclusiveChoice task in the path that leads towards it.
- **LIKELY** is like MAYBE, except it is considered to have a higher probability of being reached because the path leading towards it is the default choice in an ExclusiveChoice task.
- **FUTURE** means that the processor has predicted that this this path will be taken and this task will, at some point, definitely run. (Unless the task is explicitly set to CANCELLED, which can not be predicted.) If a task is waiting on predecessors to run then it is in FUTURE state (not WAITING).
- **WAITING** means *I am in the process of doing my work and have not finished. When the work is finished, then I will be READY for completion and will go to READY state*. WAITING is an optional state.
- **READY** means "the preconditions for marking this task as complete are met".
- **COMPLETED** means that the task is done.
- **CANCELLED** means that the task was explicitly cancelled, for example by a CancelTask operation.
Associating data with a workflow
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The difference between *specification objects* and *derivation tree objects* is also important when choosing how to store data in a workflow. Spiff Workflow supports storing data in two ways, both of which are not to be confused with their Python equivalents.
- **Properties** are stored in the TaskSpec object. In other words, if a task causes a property to change, that change is reflected to all other instances in the derivation tree that use the TaskSpec object.
- **Attributes** are local to the Task object, but are carried along to the children of each Task object in the derivation tree.
Other documentation
^^^^^^^^^^^^^^^^^^^
**API documentation** is currently embedded into the Spiff Workflow source code and not yet made available in a prettier form.
If you need more help, please drop by our `mailing list<http://groups.google.com/group/spiff-devel/>`.