Skip to content

feat(agreement): 1.0 agreement, party and runtime models (north star) - #200

Draft
mttrbrts wants to merge 1 commit into
mainfrom
claude/agreement-1.0-north-star
Draft

feat(agreement): 1.0 agreement, party and runtime models (north star)#200
mttrbrts wants to merge 1 commit into
mainfrom
claude/agreement-1.0-north-star

Conversation

@mttrbrts

@mttrbrts mttrbrts commented Aug 23, 2026

Copy link
Copy Markdown
Member

Closes #N/A

Proposes org.accordproject.agreement@1.0.0 as the successor to the contract@1.0.0 namespace in #197, together with the party and runtime models it depends on, and updates the obligation models in #196 to share its types.

This is a design target, not a migration-ready change. It exists so the structural questions behind a stable 1.0 release can be reviewed together rather than one PR at a time. It breaks every existing template. See #199 for a companion PR that promotes the same namespaces to 1.0 without breaking anything, and the migration cost table below for what the gap between them actually is.

Changes

  • agreement@1.0.0 replaces contract@1.0.0. An agreement is a set of documents rather than a single instrument, so a master agreement with schedules and confirmations is expressible.
  • party@1.0.0 defines Party and its portable PartyRef projection, so the 1.0 family has one party representation instead of the three it has today (--> Party, obligation's PartyRef, signature's Signatory).
  • Clause instances are addressed by TemplateMark instance path in a map (payment, deliveries[2].inspection) rather than by a static tree. A ClauseDefinition nested in a ListBlockDefinition yields one instance per data element, and one nested in a ConditionalDefinition may yield none, so the instance count is data-driven and a static tree cannot express it. CiceroMark remains authoritative for structure; this model records provenance over it.
  • Template data is carried by composition in TemplateData rather than by subclassing the agreement envelope.
  • Runtime state is one revisioned document per agreement, with per-clause state keyed by the same instance paths.
  • obligation@1.0.0 drops its local PartyRef and AgreementRef for the shared types.

Flags

  • Requires Concerto 4, because of the map types. This drops the "syntactically compatible with Concerto 3" property that feat(contract): add contract 1.0 model #197 has, and the requirement is contagious: the build parses a model's imports with the parser chosen for the importing file, so obligation@1.0.0 needs ^4.0.0 even though it declares no map itself.
  • ClauseState is a workaround, not a modelling decision. The map would otherwise take StateData directly. A map value whose concrete type is imported from another namespace cannot be (de)serialized — see fix(concerto-core): serialize maps with cross-namespace concept values concerto#1279, open and unreleased as of 4.1.5 — and a template's StateData subtype always is. Collapse it once that ships.
  • Publish order is a hard chain, since the build resolves imports over HTTP: partyagreementruntimeobligationobligation/settlement.
  • contract@1.0.0 must never be published if this direction is taken. Model URLs are permanent, and a dead namespace is worse than the rename.
  • Obligations may now be created without template provenance. The shared AgreementReference.template is optional, where obligation's local AgreementRef required templateId and templateHash. An agreement recorded across several documents has no single template, so the stricter rule was not satisfiable.
  • Deliberately deferred to profiles, documented in the model but not implemented: order of precedence, supersession and amendment, incorporation by reference, cross-level hash commitment, the party role vocabulary, and template artifact manifests.
  • Hashes commit only to their own level. There is no Merkle relationship between an agreement, its documents and its clauses.

Migration cost for existing templates

Measured against the 37 active templates in accordproject/cicero-template-library. The library's other 20 templates carry "archived": true in their package.json and are excluded from its index page by run.js, so they are excluded here too.

what changes active templates affected
asset TemplateModel extends Contract|Clause becomes concept … extends TemplateData 37 of 37
extends Clause has no direct analogue; a standalone clause template becomes a document with data 22
extends runtime@0.2.0.Obligation (an event) becomes an obligation@1.0.0 asset 16
extends State becomes extends StateData, composed rather than subclassed 10
extends Request / extends Response — unchanged 32 / 31

Every active template subclasses Contract or Clause, so the inheritance-to-composition change reaches all of them with no exceptions.

Composition of templates, which this model is largely built to support, is used by exactly 1 of 37 active templates today (copyright-license). The case for it is forward-looking rather than a fix for current pain, and that is worth being explicit about.

Screenshots or Video

Not applicable. Concerto model changes only.

Related Issues

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Ensure that CI passes

🤖 Generated with Claude Code

Proposes org.accordproject.agreement@1.0.0 as the successor to the
contract@1.0.0 namespace, with the party and runtime models it depends on
and the obligation models updated to share its types.

This is a design target, not a migration-ready change. It settles the
structural questions that a stable 1.0 release needs answered, so that the
answers can be reviewed together rather than one PR at a time.

- Rename contract@1.0.0 to agreement@1.0.0, and model an agreement as a
  set of documents rather than a single instrument.
- Add party@1.0.0 defining Party and its portable PartyRef projection, so
  the 1.0 family has one party representation instead of three.
- Address clause instances by TemplateMark instance path in a map, rather
  than by a static tree, since clause instances are data-driven.
- Carry template data by composition in TemplateData rather than by
  subclassing the agreement envelope.
- Hold runtime state as one revisioned document per agreement, with
  per-clause state keyed by the same instance paths.
- Point obligation@1.0.0 at the shared PartyRef and AgreementReference.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Matt Roberts <code@rbrts.uk>
mttrbrts pushed a commit to accordproject/cicero-template-library that referenced this pull request Aug 23, 2026
Prototypes migrating copyright-license's nested PaymentClause onto the
model design proposed in accordproject/models#200 ("Agreement 1.0 Model
Redesign"): composition over inheritance (TemplateData/ClauseData composed
into an AgreementDocument instead of subclassing Contract/Clause), a
unified Party type replacing the duplicated licensee*/licensor* string
fields, and clause instances addressed by path through a Concerto 4 Map.

The vendored party@0.1.0/agreement@0.1.0 models, sample.json round-trip,
and trigger() logic all work end to end. Natural-language rendering is
marked as an expected failure: markdown-template's grammar resolution
only looks at properties declared directly on the @template class and
never walks the supertype chain, so a composed/inherited envelope isn't
renderable with today's tooling -- consistent with that PR being flagged
"a design target, not migration-ready". Details are in the model file
comments and the template's README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LWZaJoaq9HFMVceNxCXMS
Signed-off-by: Claude <noreply@anthropic.com>
@mttrbrts

Copy link
Copy Markdown
Member Author

Findings from the prototype migration, and four proposed changes

cicero-template-library#528 migrates copyright-license onto this design. It surfaced two problems — duplicated clause data in sample.json, and a grammar that has to wrap its whole body in {{#with data}}. Both are worth acting on here, though only one is a defect in this PR.

The duplication is a divergence from this PR, not a flaw in it

Clause here has no data property — it was removed during review precisely because a clause's data is a subtree of the document's data. The prototype re-added it, along with a ClauseData type that doesn't exist in this namespace:

// this PR                            // prototype's agreement.cto
concept Clause {                      concept Clause {
  o String path                         o String path
  o TemplateReference template          o ClauseData data   <- reintroduced
  o String clauseId optional          }
  o ContentHash clauseHash optional
}

The cost shows up in the prototype's logic.ts, which cannot tell which copy is authoritative:

const paymentTerms = (clauses['paymentTerms']?.data as IPaymentTerms) ?? licenseData.paymentTerms;

But it exposed a real gap in this PR. Clause.template is required, because Clause means "a sub-template archive composed into this document". copyright-license has no sub-template: its old PaymentClause extends Clause was nested data carrying an id, and {{#clause paymentTerms}} is an inline grammar block. Its clauses map should therefore be absent, not populated with a duplicate. This PR never says so plainly, which is how it came to be read the other way. Worth noting that copyright-license is the one active template that does not exercise composition of templates, so it is a harder migration target than it looks.

The {{#with data}} wrapper is caused by @template sitting on the envelope

The prototype declares @template asset TemplateModel extends AgreementDocument {}, so the grammar's root is the envelope and every variable is one level down under data.

That is not only verbose, it cannot work at all. In markdown-template/lib/TypeVisitor.js:

construct line lookup walks supertypes
VariableDefinition 184 getProperty yes
ClauseDefinition 219 getOwnProperty no
WithDefinition 249 getOwnProperty no

Confirmed against concerto-core: for Sub extends AgreementDocument {}, getProperty("data") returns true but getOwnProperty("data") returns false. So {{#with data}} on a subclass with an empty body can never resolve, which is why the prototype's render tests are expected failures. Plain {{variable}} works through inheritance; block constructs do not.

Proposed fix: the renderable root is the TemplateData subtype

@template
concept CopyrightLicenseData extends TemplateData {
  o DateTime effectiveDate
  o PartyRef licensee
  o PartyRef licensor
  o String territory
  o String purposeDescription
  o String workDescription
  o PaymentTerms paymentTerms
}

This resolves all three symptoms together:

  • no {{#with}} — the grammar roots at the data, so {{effectiveDate}} resolves directly
  • no getOwnProperty failure — the @template class declares its properties directly, so nothing depends on the supertype walk
  • no duplication — sample.json's root becomes the data, so there is no envelope and no clauses map in it

The division of labour becomes explicit: a template author writes data; the runtime wraps it in a provenance envelope. The envelope keeps the stable $class that motivated composition in the first place, and the template author never has to see it.

Template data should use PartyRef, not --> Party

The prototype hand-rolls a resolveParty() helper to walk the parties array, because a relationship arrives at trigger() as a "resource:...#me" string with no resolution step. That is exactly what PartyRef is for in party@1.0.0 — a portable, embedded party value for records that travel standalone, and template data is such a record. --> Party belongs on the envelope's AgreementParty, where a resolver exists. Making this change deletes resolveParty entirely.

Changes to make in this PR

  1. Document the @template placement rule: the renderable root is the TemplateData subtype, never the envelope.
  2. State that clauses records composed sub-template archives only; inline {{#clause}} blocks get no entry.
  3. Pin the root of Clause.path. The prototype used "data.paymentTerms"; this PR intends "paymentTerms". Moving @template onto the data makes that unambiguous.
  4. Note that template data should carry PartyRef rather than --> Party.

Points 1 and 3 are the substantive ones: they mean the agreement envelope is a runtime concern rather than something a template author writes, which is a clearer statement of the design than the PR currently makes.

Not pushed as a commit — raising for discussion first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant