feat(agreement): 1.0 agreement, party and runtime models - #200
Conversation
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>
Findings from the prototype migration, and four proposed changescicero-template-library#528 migrates The duplication is a divergence from this PR, not a flaw in it
The cost shows up in the prototype's const paymentTerms = (clauses['paymentTerms']?.data as IPaymentTerms) ?? licenseData.paymentTerms;But it exposed a real gap in this PR. The
|
| 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
getOwnPropertyfailure — the@templateclass 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 noclausesmap 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
- Document the
@templateplacement rule: the renderable root is theTemplateDatasubtype, never the envelope. - State that
clausesrecords composed sub-template archives only; inline{{#clause}}blocks get no entry. - Pin the root of
Clause.path. The prototype used"data.paymentTerms"; this PR intends"paymentTerms". Moving@templateonto the data makes that unambiguous. - Note that template data should carry
PartyRefrather 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.
3a0f758 to
cbff6f2
Compare
🧩 Concerto Playground PreviewThe
All 7 changed models together: Preview combined Auto-generated from commit 8095087. Updates automatically on new commits. |
Signed-off-by: Niall Roche <niall.roche@gmail.com>
Signed-off-by: Niall Roche <niall.roche@gmail.com>
Signed-off-by: Niall Roche <niall.roche@gmail.com>
Signed-off-by: Niall Roche <niall.roche@gmail.com>
Builds on the contract, runtime and obligation 1.0 models from #197, #198 and #196, which are included here unchanged as the commits beneath this one. This commit is the delta: it keeps their type inventory and reworks the structure underneath it. 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, so that a master agreement with schedules and confirmations is expressible. - 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: a list block yields one instance per element and a conditional may yield none. - Carry template data by composition in TemplateData rather than by subclassing the agreement envelope, so the envelope's type and its relationship URIs stay stable across template versions. - 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 in place of its local equivalents. contract@1.0.0 is deleted rather than kept alongside agreement@1.0.0. Model URLs are permanent, so it must never be published if this direction is taken: a dead namespace is worse than the rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Roberts <code@rbrts.uk>
cbff6f2 to
34df2fd
Compare
…ate wrapper Two changes that the 1.0 models were waiting on, plus the namespace split that falls out of the first. Collapse ClauseState. runtime@1.0.0.ClauseStates took a single-property ClauseState wrapper rather than StateData directly, because a map value whose concrete type came from another namespace could not be serialized, and a template's StateData subtype always does. accordproject/concerto#1279 shipped in concerto-core 4.2.0, so the wrapper is gone and the map says what it means. Verified on the real models: an AgreementState carrying two template-defined clause states, keyed by instance path, round-trips losslessly on 4.2.0 and fails on 4.1.4. The declared range moves to ^4.2.0, since that is now genuinely the floor, and the build's 4.x slot moves with it so the declaration resolves rather than silently falling back to the default parser. Extract template@1.0.0. TemplateArtifactRole, TemplateArtifact and TemplateReference leave agreement@1.0.0 for a namespace of their own. They describe an archive, not an agreement, and they are cited by records that hold no agreement: obligations, attestations and settlement evidence all name the template an artefact derives from, and a registry may catalogue templates with no agreement in sight. The new namespace imports only the hash types, so naming a template no longer pulls in the agreement model. Publish order becomes: party, template, agreement, runtime, obligation, obligation/settlement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Roberts <code@rbrts.uk>
The two extension points every template subclasses no longer live in the namespaces that consume them. TemplateData sat in agreement@1.0.0 and StateData in runtime@1.0.0, so declaring the shape of a template's own data meant importing the agreement envelope or the runtime state envelope, and the party, hash and map types they carry. That is the wrong dependency direction: a template subclassing TemplateData is describing its variables, not asserting anything about agreements, documents or clause structure. Both are now in data@1.0.0, which declares nothing but the two empty abstract concepts and imports nothing at all. A template that subclasses them takes on no envelope and, because the map types requiring Concerto 4 are in the consumers rather than the extension points, stays on Concerto 3 syntax. runtime@1.0.0.ClauseStates now takes a value type that is itself imported, as well as carrying concrete types from a template's namespace. Both are handled by concerto-core 4.2.0, which the namespace already requires. Publish order becomes: data, party, template, agreement, runtime, obligation, obligation/settlement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Roberts <code@rbrts.uk>
|
thanks for consolidating into this PR Matt and for catching the 'runtime@0.2.0' dependency. I am happy for agreement@1.0.0 to replace contract@1.0.0, and to close #197. Here are some observations: 1. Hash rules
This is the same point I raised about The awkward part is that optional-to-required is a breaking change, so whichever way 1.0 publishes is effectively permanent. That suggests either making the four optional in the core models and letting a deterministic profile require them and define the rules, or defining the canonicalization normatively here. I have a working canonicalization for this in the x402 settlement work — RFC 8785 JCS over Concerto JSON with the hash field excluded, with golden and tamper vectors. I would be glad to write it up as a profile PR against whichever of the two routes you prefer, so it is not extra work landing on you. 2. Finishing the party consolidationCollapsing the three party representations is clearly right, and One point on signature:
I do not think it belongs on 3.
|
Responds to the review on #200. Hash canonicalization is not defined by these models, so agreementHash, documentHash, stateHash and AgreementReference's agreementHash become optional. Two implementations hashing the same agreement under different rules would both be conformant today, and optional to required is the breaking direction, so the core models leave the properties open and a deterministic profile fixes the canonicalization and may require them. Where a hash is supplied its crypto@1.0.0 Canonicalization should be populated, so a verifier can tell which discipline produced it. Clause.path is dropped. It restated the key of the AgreementDocument.clauses map with nothing requiring the two to agree. The key is now the single statement of a clause's instance path, and records citing a clause from outside the map carry the path themselves, as AgreementReference already does. The data@1.0.0 namespace becomes templatedata@1.0.0. It holds exactly the two types templates extend, and a generic namespace name is not one this repository should claim for it. TemplateReference.identifier becomes templateId, which names the template rather than the reference. Identifier syntax remains deferred to a profile. AgreementState is documented as intentionally registry-resident: a revision counter and a previous-state hash chain are only meaningful where successive revisions resolve against one another, which is why it holds a relationship rather than the portable AgreementReference that obligations and settlement evidence use. PartyRef is documented as deliberately carrying no role. A role is a property of an act, not of a party, so it belongs on AgreementParty and on attestations rather than on the portable party projection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r Signed-off-by: Matt Roberts <matt@rbrts.uk>
Follows up the prototype migration findings raised on #200. Doc comments only; no structural change. The @template decorator belongs on the TemplateData subtype, never on the agreement envelope. This is not a matter of taste: markdown-template resolves clause and with blocks with getOwnProperty, which does not walk supertypes, so {{#with data}} on an empty subclass of an envelope never resolves and the template cannot render at all. Rooting the grammar at the data removes the wrapper, and makes the division of labour explicit: a template author writes data, and the runtime wraps it in a provenance envelope the author never sees. The clauses map records sub-template archives composed into a document, not the document's clause structure. An inline clause block, written in the parent grammar and composing no separate archive, gets no entry, and a document whose clause blocks are all inline has no clauses map rather than an empty one. This is why Clause.template is required. Clause instance paths are rooted at the document's data rather than at the document, so a clause bound to the paymentTerms property has the path "paymentTerms" and never "data.paymentTerms". Template data should name parties with party@1.0.0's PartyRef rather than a relationship to Party. Data reaches contract logic as a serialized value with no resolution step, where a relationship arrives as an unresolvable "resource:...#me" string. Relationships to Party belong on the envelope, where a registry can resolve them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r Signed-off-by: Matt Roberts <matt@rbrts.uk>
Closes #N/A
Proposes the
1.0family of Accord Project models: an agreement is a set of documents rather than a single instrument, a template is identified independently of anything created from it, and the types a template extends are held apart from the envelopes that carry them.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 as a whole rather than one namespace at a time. It builds on #197, #198 and #196, whose commits are included beneath it.
The namespaces
data@1.0.0TemplateData,StateData^3.0.0party@1.0.0Party,PartyRef^3.0.0template@1.0.0TemplateReference,TemplateArtifact,TemplateArtifactRolecrypto^3.0.0agreement@1.0.0Agreement,AgreementDocument,Clause,Clauses,AgreementParty,AgreementReferencecrypto,data,party,template^4.0.0runtime@1.0.0Request,Response,AgreementState,ClauseStatescrypto,data,agreement^4.2.0obligation@1.0.0Obligationand its subtypes,ObligationTransition,FulfilmentAttemptcrypto,money,party,agreement^4.0.0obligation/settlement@1.0.0SettlementObligation,SettlementEvidence, and the evidence conceptscrypto,party,obligation^4.0.0The Concerto floors follow the map types: only
agreementandruntimedeclare one.runtimeneeds 4.2.0 specifically because both the declared value type ofClauseStatesand the concrete types an instance carries are imported from other namespaces.agreement@1.0.0replaces thecontract@1.0.0namespace proposed in #197.An agreement is a set of documents
A master agreement with schedules and confirmations is one agreement recorded across several instruments.
Agreementholds an ordered list ofAgreementDocument, each of which may bind its own subset of the parties and may be generated from its own template — or from none, where an instrument binds sub-templates per clause without a document-level template of its own.Clause instances are addressed by instance path
AgreementDocument.clausesis a map keyed by the TemplateMark instance path of each clause:payment, ordeliveries[2].inspection. A static clause tree cannot express this, because the number of instances is decided by the data — aClauseDefinitioninside aListBlockDefinitionyields one instance per element, and one inside aConditionalDefinitionmay yield none.CiceroMark remains authoritative for document structure. This model records provenance over it and does not restate it.
Template data is carried by composition
A template declares one concrete subtype of
data@1.0.0.TemplateDataholding its instantiated variables, and that subtype is carried inAgreementDocument.data. A template no longer subclasses the agreement envelope, so the envelope's type and its relationship URIs stay stable across template versions.The data of a clause is a subtree of its document's data, addressed by the clause's instance path. A clause carries no copy of its own.
Runtime state is one revisioned document per agreement
AgreementStateis a single identified, revisioned snapshot covering the agreement and every clause instance in it, withclauseStateskeyed by the same instance paths asAgreementDocument.clauses. A transition spanning scopes — marking a clause breached because of a condition detected in a peer clause — is then one write producing one revision and one hash, with sibling state in scope without a further lookup. Per-clause state assets would need a cross-asset transaction Concerto does not provide, and would leaverevisionandpreviousStateHashwithout a well-defined meaning.data@1.0.0holds only what templates extendTemplateDataandStateDataare empty abstract concepts, and they are the two types every template subclasses. Holding them in a namespace that imports nothing means declaring the shape of a template's own data does not drag in the agreement envelope, the state envelope, or the party and hash types those carry.The template quoted under Engine compatibility below is the whole of the effect: it declares
concerto version "^3.0.0", and its imports aredata@1.0.0,runtime@0.2.0andmoney@0.3.0— no agreement, no crypto, no party, and no Concerto 4 floor, because the map types requiring it are in the consumers rather than the extension points.template@1.0.0is independent of agreementsA template is cited by records that hold no agreement — obligations, attestations and settlement evidence all name the template an artefact derives from, and a registry may catalogue templates with no agreement in sight.
template@1.0.0imports only the hash types, so naming a template does not require taking on the agreement model.One party representation
party@1.0.0definesParty, which is registry-resident, andPartyRef, a portable projection for records that must travel standalone and cannot assume a resolver. The 1.0 family uses these two throughout, in place of the three representations in use today (--> Party, obligation's localPartyRef, signature'sSignatory).Engine compatibility
Measured against unmodified
cicero-core@2.1.1andtemplate-engine@4.0.0.Composition works on today's engine. A
@templateconcept that extendsdata@1.0.0.TemplateDatarather than an agreement asset, with nested clauses that are plain concepts, loads, validates and renders with no engine change:Neither half of the engine requires
ContractorClauseinheritance.findTemplateConceptlooks for exactly one non-abstract declaration carrying@template, and{{#clause X}}resolves propertyXon the current model and recurses.Concerto 4 is not a consumer-side constraint.
cicero-core@2.1.1andtemplate-engine@4.0.0both already bundleconcerto-core@4.1.4, and map-bearing models load and render today.runtime@1.0.0's 4.2.0 floor is real but confined to it: on 4.1.4 anAgreementStatecarrying template-defined clause states fails to serialize, and on 4.2.0 the same instance round-trips with every key and$classpreserved (accordproject/concerto#1279).Adopting
runtime@1.0.0needs acicero-corechange.Templateresolves the runtime base types by fully-qualified name againstruntime@0.2.0, andgetType()throws when a namespace is absent, so a template on a 1.0 runtime namespace cannot be opened at all. This is version-bump coupling that lands on anyruntime@1.0.0, including the conservative promotions in #198 and #199. Fixed by accordproject/template-archive#946.Landing order
Publish order is a hard chain, since the build resolves imports over HTTP:
data,party,template,agreement,obligationandobligation/settlementcan land and publish independently of the engine. Templates adopt the 1.0 data shape while keepingruntime@0.2.0forRequestandResponse.runtime@1.0.0should follow the release of accordproject/template-archive#946. Publishing it earlier creates a namespace no template can adopt, and model URLs are permanent.For the same reason
contract@1.0.0must never be published if this direction is taken. A dead namespace is worse than the rename.Migration cost
Measured against the 37 active templates in
accordproject/cicero-template-library. The library's other 20 carry"archived": trueand are excluded from its index page byrun.js, so they are excluded here too.asset TemplateModel extends Contract|Clausebecomesconcept … extends TemplateDataextends Clausehas no direct analogue; a standalone clause template becomes a document with dataextends runtime@0.2.0.Obligation(an event) becomes anobligation@1.0.0assetextends Statebecomesextends StateData, composed rather than subclassedextends Request/extends Response— unchangedEvery active template subclasses
ContractorClause, so the inheritance-to-composition change reaches all of them without exception. It is a mechanical edit that today's engine already accepts.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.Deliberately not modelled
Each of these was considered and left to an extension profile. The models document what a profile would have to define.
Agreement.documentsholds reading order, and precedence is commonly its inverse.AgreementParty.roleis an open string.TemplateReference.artifactsis populated, a profile must define the manifest canonicalization and require the list to agree withartifactManifestHash.Other notes
AgreementReference.templateis optional, where obligation's localAgreementRefrequiredtemplateIdandtemplateHash. An agreement recorded across several documents has no single template, so the stricter rule was not satisfiable.runtime@1.0.0drops the deprecatedruntime@0.2.0.Obligationevent. 21 templates subclass it;obligation@1.0.0is the destination.data@1.0.0is a working name.templatedataorextensionare equally defensible.Screenshots or Video
Not applicable. Concerto model changes only.
Related Issues
Author Checklist
--signoffoption of git commit.🤖 Generated with Claude Code