Skip to content

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

Open
mttrbrts wants to merge 10 commits into
mainfrom
claude/agreement-1.0-north-star
Open

feat(agreement): 1.0 agreement, party and runtime models#200
mttrbrts wants to merge 10 commits 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 the 1.0 family 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

namespace holds imports Concerto
[email protected] TemplateData, StateData nothing ^3.0.0
[email protected] Party, PartyRef nothing ^3.0.0
[email protected] TemplateReference, TemplateArtifact, TemplateArtifactRole crypto ^3.0.0
[email protected] Agreement, AgreementDocument, Clause, Clauses, AgreementParty, AgreementReference crypto, templatedata, party, template ^4.0.0
[email protected] Request, Response, AgreementState, ClauseStates crypto, templatedata, agreement ^4.2.0
[email protected] Obligation and its subtypes, ObligationTransition, FulfilmentAttempt crypto, money, party, agreement ^4.0.0
obligation/[email protected] SettlementObligation, SettlementEvidence, and the evidence concepts crypto, party, obligation ^4.0.0

The Concerto floors follow the map types: only agreement and runtime declare one. runtime needs 4.2.0 specifically because both the declared value type of ClauseStates and the concrete types an instance carries are imported from other namespaces.

[email protected] replaces the [email protected] namespace proposed in #197.

An agreement is a set of documents

A master agreement with schedules and confirmations is one agreement recorded across several instruments. Agreement holds an ordered list of AgreementDocument, 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.clauses is a map keyed by the TemplateMark instance path of each clause: payment, or deliveries[2].inspection. A static clause tree cannot express this, because the number of instances is decided by the data — a ClauseDefinition inside a ListBlockDefinition yields one instance per element, and one inside a ConditionalDefinition may yield none.

The path is held only in the map key. Clause carries no path property, so there is one statement of it and no invariant tying two copies together.

The 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 — a document whose clause blocks are all inline has no clauses map rather than an empty one. This is why Clause.template is required.

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 [email protected] holding its instantiated variables, and that subtype is carried in AgreementDocument.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. Instance paths are rooted at the data rather than at the document, so a clause bound to the paymentTerms property has the path paymentTerms and never data.paymentTerms.

The template root is found by its parent type

Extending TemplateData is what declares a template's data. A template's model holds exactly one concrete subtype of it, and that subtype is the renderable root. No @template decorator marks it: the supertype already carries that meaning, and an annotation restating it is a second statement of the same fact that nothing keeps in agreement with the first. Types the data is composed of stay ordinary concepts; only the root extends TemplateData. StateData is resolved the same way.

The root must be the data and not an envelope, and that is a hard constraint rather than a preference: 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. Plain variables resolve through inheritance; block constructs do not.

Runtime state is one revisioned document per agreement

AgreementState is a single identified, revisioned snapshot covering the agreement and every clause instance in it, with clauseStates keyed by the same instance paths as AgreementDocument.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 leave revision and previousStateHash without a well-defined meaning.

AgreementState is registry-resident, and intentionally so. It holds a relationship to its Agreement rather than the portable AgreementReference that obligations and settlement evidence use, because a revision counter and a previous-state hash chain are only meaningful where successive revisions resolve against one another. Records that must travel standalone reference an agreement portably; state does not travel, so it does not need to. This mirrors the Party / PartyRef distinction.

[email protected] holds only what templates extend

TemplateData and StateData are 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 are [email protected], [email protected] and [email protected] — 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.

[email protected] is independent of agreements

A 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. [email protected] imports only the hash types, so naming a template does not require taking on the agreement model.

TemplateReference.templateId names the template rather than the reference. The type is deliberately not Concerto-identified, so templateId carries no identity semantics of its own, and its syntax remains deferred to a profile.

One party representation

[email protected] defines Party, which is registry-resident, and PartyRef, 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 local PartyRef, signature's Signatory).

PartyRef deliberately carries no role. A role is a property of an act, not of a party: the same party may sign in different roles on different documents. Role therefore belongs on AgreementParty for a party's role under an agreement, and on an attestation for the capacity in which a signature was given.

Template data should name parties with 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.

Hashes are optional and their canonicalization is deferred

Agreement.agreementHash, AgreementDocument.documentHash, AgreementState.stateHash and AgreementReference.agreementHash are optional, and these models do not define how the bytes they commit to are produced. Two implementations hashing the same agreement under different canonicalization rules would both be conformant, so a hash is meaningful only under a profile that fixes those rules.

The properties are optional rather than required because optional-to-required is the breaking direction and this family publishes permanently: a profile can tighten what the core model leaves open, and nothing can loosen it afterwards. Where a hash is supplied, its [email protected] Canonicalization should be populated, so a verifier can tell which discipline produced it rather than having to assume one. CanonicalizationType already carries RFC8785_JCS.

Engine compatibility

Measured against unmodified [email protected] and [email protected].

Composition renders. A concept that extends [email protected] rather than an agreement asset, with nested clauses that are plain concepts, loads, validates and renders with no change to the render path:

templateModel: [email protected] | super: [email protected]
requestTypes: [ '[email protected]', '[email protected]' ]
sample validates: OK
--- RENDER ---
Agreement with Acme Corporation for a fee of 15000.0 USD.
Confidentiality. Term of 5 years.

Neither half of the engine requires Contract or Clause inheritance. {{#clause X}} resolves property X on the current model and recurses, and nothing in the render path depends on the envelope.

Finding the template root needs a cicero-core change. That measurement was taken with @template still on the data concept, which is how findTemplateConcept locates the renderable root today: it looks for exactly one non-abstract declaration carrying the decorator. This design no longer uses it, so the root has to be resolved by parent type instead. Until that ships, a template written to this design has no root the published engine can find. Fixed by accordproject/template-archive#946.

Concerto 4 is not a consumer-side constraint. [email protected] and [email protected] both already bundle [email protected], and map-bearing models load and render today. [email protected]'s 4.2.0 floor is real but confined to it: on 4.1.4 an AgreementState carrying template-defined clause states fails to serialize, and on 4.2.0 the same instance round-trips with every key and $class preserved (accordproject/concerto#1279).

Adopting [email protected] needs the same change. Template resolves the runtime base types by fully-qualified name against [email protected], and getType() 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 any [email protected], including the conservative promotions in #198 and #199. Also fixed by accordproject/template-archive#946.

Landing order

Publish order is a hard chain, since the build resolves imports over HTTP:

templatedata → party → template → agreement → runtime → obligation → obligation/settlement

templatedata, party, template, agreement, obligation and obligation/settlement can land and publish independently of the engine. Templates adopt the 1.0 data shape while keeping [email protected] for Request and Response.

[email protected] should 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 [email protected] must 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": true and are excluded from its index page by run.js, so they are excluded here too.

what changes active templates affected needs an engine change?
asset TemplateModel extends Contract|Clause becomes concept … extends TemplateData 37 of 37 yes — template-archive#946
@template is dropped; the root is found by its TemplateData supertype 37 of 37 yes — template-archive#946
extends Clause has no direct analogue; a standalone clause template becomes a document with data 22 no
extends [email protected] (an event) becomes an [email protected] asset 16 no
extends State becomes extends StateData, composed rather than subclassed 10 yes — template-archive#946
extends Request / extends Response — unchanged 32 / 31 n/a

Every active template subclasses Contract or Clause, so the inheritance-to-composition change reaches all of them without exception. The edit itself is mechanical, but it is no longer one today's engine accepts unaided: dropping the decorator moves root resolution onto the supertype, so the whole migration is now gated on template-archive#946 rather than only the state and runtime parts of it. That is a real change in cost from an earlier revision of this PR, which measured the composition shape with the decorator still present.

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.

  • Hash canonicalization — how the bytes a hash commits to are produced, and that the hash property is excluded from its own pre-image. A deterministic profile must define this and may require the four hash properties this model leaves optional.
  • Order of precedence — which document prevails where two conflict. Agreement.documents holds reading order, and precedence is commonly its inverse.
  • Supersession and amendment — a link between documents rather than an ordering over them.
  • Incorporation by reference — including terms published elsewhere, and whether as at a fixed version or as amended from time to time.
  • Cross-level hash commitment — every hash commits only to its own level. There is no Merkle relationship between an agreement, its documents and its clauses.
  • Party role vocabularyAgreementParty.role is an open string.
  • Template identifier syntax — whether TemplateReference.templateId is a name, a URI or a registry key, and how it is compared.
  • Template artifact manifests — where TemplateReference.artifacts is populated, a profile must define the manifest canonicalization and require the list to agree with artifactManifestHash.

Other notes

  • Obligations may be created without template provenance. 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.
  • [email protected] drops the deprecated [email protected] event. 21 templates subclass it; [email protected] is the destination.
  • [email protected] was [email protected] in an earlier revision. data was too generic a namespace for this repository to claim for two concepts, and the name is free to change now and impossible to change after publication.

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

https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r

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 [email protected]/[email protected] 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_011LWZaJoaq9HFMVceNxCXMS
Signed-off-by: Claude <[email protected]>
@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 [email protected] — 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.

@mttrbrts
mttrbrts force-pushed the claude/agreement-1.0-north-star branch from 3a0f758 to cbff6f2 Compare August 24, 2026 08:00
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

🧩 Concerto Playground Preview

The .cto model changes in this PR are pre-loaded into the Concerto Playground — click a link below for a live preview.

File Preview
src/accordproject/[email protected] Diagram · Code · Open in Playground
src/accordproject/obligation/[email protected] Diagram · Code · Open in Playground
src/accordproject/[email protected] Diagram · Code · Open in Playground
src/accordproject/[email protected] Diagram · Code · Open in Playground
src/accordproject/[email protected] Diagram · Code · Open in Playground
src/accordproject/[email protected] Diagram · Code · Open in Playground
src/accordproject/[email protected] Diagram · Code · Open in Playground

All 7 changed models together: Preview combined

Auto-generated from commit e8e0fb3. Updates automatically on new commits.

niallroche and others added 5 commits August 27, 2026 09:16
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 [email protected] to [email protected], 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 [email protected] 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 [email protected] at the shared PartyRef and AgreementReference in
  place of its local equivalents.

[email protected] is deleted rather than kept alongside [email protected].
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 <[email protected]>
Signed-off-by: Matt Roberts <[email protected]>
@mttrbrts
mttrbrts force-pushed the claude/agreement-1.0-north-star branch from cbff6f2 to 34df2fd Compare August 27, 2026 08:19
@mttrbrts mttrbrts changed the title feat(agreement): 1.0 agreement, party and runtime models (north star) feat(agreement): 1.0 agreement, party and runtime models Aug 27, 2026
mttrbrts and others added 2 commits August 27, 2026 10:25
…ate wrapper

Two changes that the 1.0 models were waiting on, plus the namespace split
that falls out of the first.

Collapse ClauseState.
  [email protected] 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 [email protected].
  TemplateArtifactRole, TemplateArtifact and TemplateReference leave
  [email protected] 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 <[email protected]>
Signed-off-by: Matt Roberts <[email protected]>
The two extension points every template subclasses no longer live in the
namespaces that consume them.

TemplateData sat in [email protected] and StateData in [email protected], 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 [email protected], 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.

[email protected] 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 <[email protected]>
Signed-off-by: Matt Roberts <[email protected]>
@mttrbrts
mttrbrts marked this pull request as ready for review August 27, 2026 10:04
@mttrbrts
mttrbrts requested a review from a team August 27, 2026 12:37
@niallroche

Copy link
Copy Markdown
Contributor

thanks for consolidating into this PR Matt and for catching the '[email protected]' dependency. I am happy for [email protected] to replace [email protected], and to close #197.

Here are some observations:

1. Hash rules

Agreement.agreementHash, AgreementDocument.documentHash, AgreementState.stateHash and AgreementReference.agreementHash are each documented as committing to "the canonical representation, excluding the property itself", and the canonicalization is not yet defined. As it stands two conformant implementations could produce different hashes for the same agreement and neither would be wrong.

This is the same point I raised about stateHash on #198, and I mention it again only because the surface has grown from one field to four, which makes it worth settling deliberately rather than by default.

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 consolidation

Collapsing the three party representations is clearly right, and Party registry-resident with PartyRef portable is a cleaner statement of the distinction than I managed — scheme in particular is a real improvement on the bare identifier I had in Signatory.

One point on signature:

[email protected] Signatory { identifier, name optional, role optional, uri optional }
[email protected]     PartyRef  { id, scheme, label optional }

role is doing work in attestations — it distinguishes a signatory who is a party from a witness, a delegate, or a counter-signing provider, and an attestation that cannot record which is weaker as evidence.

I do not think it belongs on PartyRef, since the same party may sign in different roles on different documents. It sits more naturally on the attestation. If you agree, I will make that change in #195 as part of moving it onto [email protected], and you would not need to alter anything here.

3. Clause.path and the map key

AgreementDocument.clauses is keyed by clause instance path, and Clause also declares o String path, so the same value is recorded in both places with nothing requiring them to agree.

Dropping Clause.path and letting the key be authoritative would remove the question. If it is there for the case where a Clause travels outside its map, that is a good reason to keep it, and documenting the invariant plus a test would cover it.

4. templateId

A small consideration is that identifier inside a TemplateReference still reads to me as though it identifies the reference rather than the template, which is the confusion behind the "should this be identified?" comments on #197. Deferring identifier syntax to a profile is right and separate from what the field is called. Entirely your call.

5. Questions

  • Is AgreementState intentionally registry-bound? It holds --> Agreement, and Agreement holds --> AgreementDocument[], so state cannot travel without a resolver, while obligations and settlement evidence reference agreements portably. You state the registry-resident and portable rule explicitly for Party; the equivalent sentence for state would settle it.

  • [email protected] naming. Of your two alternatives I prefer templatedataextension names the mechanism, whereas the namespace holds exactly the two types templates extend.

Matt Roberts and others added 3 commits September 1, 2026 21:15
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
[email protected] 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 [email protected] namespace becomes [email protected]. 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r
Signed-off-by: Matt Roberts <[email protected]>
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 [email protected]'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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r
Signed-off-by: Matt Roberts <[email protected]>
The @template decorator is no longer how a template's renderable root
is identified. Extending TemplateData is the declaration: a template's
model holds exactly one concrete subtype of it, and that subtype is
found by its parent type.

The decorator restated something the inheritance already said, and
nothing kept the two in agreement. Types the data is composed of stay
ordinary concepts; only the root extends TemplateData. StateData is
resolved the same way.

The reason the root cannot be an agreement envelope is unchanged, and
is stated without reference to the decorator: 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.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r
Signed-off-by: Matt Roberts <[email protected]>
mttrbrts pushed a commit to accordproject/template-archive that referenced this pull request Sep 4, 2026
…or alone

accordproject/models#200 moved on since #946 was written: the base
namespace for a template's own data model is now [email protected]
(renamed from [email protected]), and it, not [email protected], is where
StateData lives. Template.STATE_TYPE follows the rename.

The bigger change is how the template model itself is found. A
template's model now declares exactly one concrete subtype of
[email protected], and that subtype is the template
model: extending TemplateData is the declaration, so no @template
decorator is required to identify it. getTemplateModel() resolves
this the same way getStateTypes() and friends already do, by reusing
findConcreteSubclassNames(Template.TEMPLATE_DATA_TYPE):

- exactly one concrete subtype is the template model
- more than one is rejected with an error naming the offending types
- none (the namespace predates this convention, or simply is not
  loaded) falls back to the legacy @template-decorator lookup, so
  existing decorated templates keep loading unchanged

Fixtures that declare state now extend [email protected],
and every reduced local copy of [email protected] drops StateData in
favour of a new reduced local copy of [email protected]. Added
test/data/no-template-decorator to cover the decorator-free path and
reworked test/data/multiple-concepts to declare two concrete
subtypes of TemplateData, covering the new ambiguity error.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r
Signed-off-by: Claude <[email protected]>
mttrbrts pushed a commit to accordproject/cicero-template-library that referenced this pull request Sep 4, 2026
The model design proposed in accordproject/models#200 has moved on
substantially since this prototype was written. Bring it back in line:

- No `@template` decorator, no envelope class. A template's model now
  declares exactly one concrete subtype of
  `[email protected]`, and that subtype IS the template
  model (`CopyrightLicenseData`), carrying its variables directly. This
  also fixes the specific rendering gap the previous revision hit:
  TemplateMark's TypeVisitor resolves properties via
  `ClassDeclaration#getOwnProperty`, which doesn't walk supertypes, so
  an empty `TemplateModel extends AgreementDocument {}` could never
  resolve its inherited `data`. Rooting the grammar at the data itself
  removes the `{{#with data}}` wrapper and the problem it hit.
- No `clauses` map. `copyright-license` has no sub-template archive --
  `{{#clause paymentTerms}}` is an inline grammar block in this
  template's own grammar -- so `PaymentTerms` is simply a nested
  ordinary concept, addressed as a subtree of the data. This removes
  the data-duplication `logic.ts` previously had to paper over with
  `clauses['paymentTerms']?.data ?? licenseData.paymentTerms`.
- `licensee`/`licensor` are `PartyRef` values (a portable embedded
  reference), not `--> Party` relationships. A relationship reaches
  `trigger()` as an unresolvable `"resource:...#me"` string with no
  registry to resolve it against; `resolveParty()` is gone along with
  the relationship. `--> Party` belongs on the envelope's
  `AgreementParty`, where a registry exists (see model/agreement.cto,
  which documents that wider envelope even though this template's own
  model has none to be composed onto).
- Renamed the vendored `[email protected]` stand-in to `[email protected]`,
  split into its own model/templatedata.cto per the current namespace
  split, and updated model/party.cto (adds `PartyRef`) and
  model/agreement.cto (drops `Clause.path`, adds `AgreementParty`,
  documents why `Clause`/`Clauses` are out of scope for this
  prototype) to match.

Empirically, removing `@template` fixes rendering but trades it for a
worse problem: the installed `@accordproject/cicero-core` (2.1.1, and
the copy vendored inside `@accordproject/template-engine`) finds a
template's root exclusively via the `@template` decorator, and
`Template#validate()` -- called unconditionally by
`Template.fromDirectory()` -- calls `getTemplateModel()`
unconditionally too. So without the decorator this template cannot be
*loaded* at all with today's toolchain, let alone drafted or triggered
-- confirmed empirically (see README). The fix is
accordproject/template-archive#946, which is not released.

`@template` has deliberately not been added back -- that would paper
over the exact gap this prototype exists to demonstrate. Instead,
test/render.test.mjs gains an `expectedLoadFailures` set (a strict
superset of `expectedFailures`, since a template here can't even load)
covering this template in both the render and trigger describes, each
`it.fails` naming template-archive#946, and the module-level
template-loading probe now tolerates a known load failure without
taking down the whole render-test file for every other template.

npm run test --workspaces --if-present: 49/49 workspace test files
pass (212 tests). npm run test:render: 114 passed, 5 expected fail (of
119), 0 unexpected failures.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NPtNYorvvn7MQwTovLXp2r
Signed-off-by: Claude <[email protected]>
@niallroche

Copy link
Copy Markdown
Contributor

Great @mttrbrts. Does removing the decorator need a 'cicero-core' change? Your earlier analysis had 'findTemplateConcept' looking for exactly one non-abstract declaration carrying '@template'. If the supertype now carries that meaning, the lookup has to resolve by parent type instead. The engine-compatibility result was measured with the decorator in place, so I want to check whether this adds a second engine dependency alongside template-archive#946. it seems that it matters for the migration table, which is careful about which rows need one.

@mttrbrts

mttrbrts commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Yes, you're right @niallroche. The template-archive prototype now includes the necessary changes too, accordproject/template-archive#946. Similarly, the prototype template migration in accordproject/cicero-template-library#528 aligns.

I want to validate the sample template with a clause reference to a separate template, but otherwise think that we're pretty close.

@niallroche niallroche left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Matt, that answers the decorator/root-resolution question for me. If template-archive#946 is carrying the TemplateData parent-type lookup with decorator fallback, then the engine dependency is clear and I am happy with that part of the story.

The remaining thing I would like to pin before we call this settled is the composed-template evidence. As I read it, #946 proves the engine can find/load the template data root and move to the 1.0 runtime shape, but it does not construct the runtime agreement envelope: AgreementDocument, documentHash, or the clauses provenance map.

Your note about validating a sample template with a clause reference to a separate template is exactly the missing evidence. The current migration text says composition is used by exactly 1 of 37 active templates today, citing copyright-license; but copyright-license is the inline {{#clause}} case, so it is useful migration coverage but not evidence for composed sub-template archive provenance. Could we either correct that count/wording, or add the separate-template clause sample and use that as the composition evidence?

One related sequencing question: if the envelope construction is not in #946, who owns the first implementation that wraps template data into AgreementDocument, populates documentHash, and builds the clauses map? That feels relevant to whether [email protected] can publish ahead of implementation, given the HTTP import publish chain.

Separately, I can take the APAP/LCP L3/L4 follow-up for TermsAcceptance, evidence binding, and hash-discipline alignment as its own PR.

And yes, the extra point about Template.EMIT_TYPE = '[email protected]' matters. It means [email protected] is no longer off to the side: #946’s engine change depends on it as part of the 1.0 runtime shape, so your #196 becomes part of the practical publish chain.

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.

2 participants